"""
The ExploitationFeaturesType definition.
"""

__classification__ = "UNCLASSIFIED"
__author__ = "Thomas McCullough"


import logging
import datetime
from typing import Union, List

import numpy

from sarpy.io.xml.base import Serializable, ParametersCollection
from sarpy.io.xml.descriptors import SerializableDescriptor, ParametersDescriptor, \
    FloatDescriptor, FloatModularDescriptor, StringDescriptor, StringEnumDescriptor, \
    DateTimeDescriptor, SerializableListDescriptor
from sarpy.io.complex.sicd_elements.SCPCOA import GeometryCalculator
from sarpy.io.complex.sicd_elements.SICD import SICDType
from sarpy.geometry.geocoords import wgs_84_norm, ecf_to_geodetic
import sarpy.geometry.point_projection
import sarpy.processing.sicd.windows

from .base import DEFAULT_STRICT, FLOAT_FORMAT
from .blocks import RowColIntType, RowColDoubleType, RangeAzimuthType, \
    AngleMagnitudeType, RadarModeType


TX_POLARIZATION_VALUES = ('V', 'H', 'RHC', 'LHC', 'OTHER', 'UNKNOWN', 'SEQUENCE')
RCV_POLARIZATION_VALUES = ('V', 'H', 'RHC', 'LHC', 'OTHER', 'UNKNOWN')
logger = logging.getLogger(__name__)

_sicd_type_text = 'Requires instance of SICDType,\n\tgot type `{}`'
_exp_calc_text = 'Requires input which is an instance of ExploitationCalculator,\n\tgot type `{}`'


class ExploitationCalculator(object):
    """
    Helper class for calculating the various geometric values for exploitation
    features. This is predominantly using the ontology presented in the SIDD standards
    document.
    """

    def __init__(self, geometry_calculator, row_vector, col_vector):
        """

        Parameters
        ----------
        geometry_calculator : GeometryCalculator
        row_vector : numpy.ndarray
        col_vector : numpy.ndarray
        """

        # extract sicd based parameters
        self.geometry_calculator = geometry_calculator
        self.ARPPos = geometry_calculator.ARP
        self.ARPVel = geometry_calculator.ARP_vel
        self.SCP = geometry_calculator.SCP
        self.slant_x = self._make_unit(self.ARPPos - self.SCP)
        self.slant_z = self._make_unit(numpy.cross(self.slant_x, self.ARPVel))
        if self.slant_z.dot(self.ARPPos) < 0:
            self.slant_z *= -1
        self.slant_y = self._make_unit(numpy.cross(self.slant_z, self.slant_x))
        self.ETP = wgs_84_norm(self.SCP)

        # store SIDD grid parameters
        self.row_vector = self._make_unit(row_vector)
        self.col_vector = self._make_unit(col_vector)
        self.normal_vector = self._make_unit(numpy.cross(self.row_vector, self.col_vector))

    @staticmethod
    def _make_unit(vec):
        vec_norm = numpy.linalg.norm(vec)
        if vec_norm < 1e-6:
            logger.error(
                'input vector to be normalized has norm {}, this may be a mistake'.format(vec_norm))
        return vec/vec_norm

    # The geometry properties
    @property
    def AzimuthAngle(self):
        return self.geometry_calculator.AzimAng

    @property
    def SlopeAngle(self):
        """
        float: The angle between the ground plane and slant plane. Note that the
        standard outlines this is angle between the Earth Tangent Plane and slant
        plane, with the expectation that the "ground plane" will always be defined
        as the Earth Tangent Plane.
        """

        return numpy.rad2deg(numpy.arccos(self.slant_z.dot(self.ETP)))

    @property
    def DopplerConeAngle(self):
        return self.geometry_calculator.DopplerConeAng

    @property
    def SquintAngle(self):
        return self.geometry_calculator.SquintAngle

    @property
    def GrazeAngle(self):
        """
        float: The angle between the ground plane and line of sight vector.
        """

        return numpy.rad2deg(numpy.arcsin(self.slant_x.dot(self.ETP)))

    @property
    def TiltAngle(self):
        """
        float: The angle between the ground plane and cross range vector.
        """

        return numpy.rad2deg(numpy.arctan(self.ETP.dot(self.slant_y)/self.ETP.dot(self.slant_z)))

    # The phenomenology properties
    @property
    def Shadow(self):
        # type: () -> AngleMagnitudeType
        """
        AngleMagnitudeType: The shadow angle and magnitude.
        """

        shadow = self.ETP - self.slant_x/(self.slant_x.dot(self.ETP))
        shadow_prime = shadow - (shadow.dot(self.normal_vector)/self.slant_z.dot(self.normal_vector))*self.slant_z
        return AngleMagnitudeType(
            Angle=numpy.rad2deg(numpy.arctan2(self.col_vector.dot(shadow_prime), self.row_vector.dot(shadow_prime))),
            Magnitude=numpy.linalg.norm(shadow_prime))

    @property
    def Layover(self):
        # type: () -> AngleMagnitudeType
        """
        AngleMagnitudeType: The layover angle and magnitude.
        """

        layover = self.normal_vector - self.slant_z/(self.slant_z.dot(self.normal_vector))
        return AngleMagnitudeType(
            Angle=numpy.rad2deg(numpy.arctan2(self.col_vector.dot(layover), self.row_vector.dot(layover))),
            Magnitude=numpy.linalg.norm(layover))

    @property
    def North(self):
        """
        float: The north angle.
        """

        lat, lon, hae = ecf_to_geodetic(self.SCP)
        lat_r = numpy.deg2rad(lat)
        lon_r = numpy.deg2rad(lon)
        north = numpy.array(
            [-numpy.sin(lat_r)*numpy.cos(lon_r),
             -numpy.sin(lat_r)*numpy.sin(lon_r),
             numpy.cos(lat_r)])
        north_prime = north - self.slant_z*(north.dot(self.normal_vector)/self.slant_z.dot(self.normal_vector))
        return numpy.rad2deg(numpy.arctan2(self.col_vector.dot(north_prime), self.row_vector.dot(north_prime)))

    @property
    def MultiPath(self):
        """
        float: The multipath angle.
        """

        multipath = self.slant_x - self.slant_z*(
                self.slant_x.dot(self.normal_vector)/self.slant_z.dot(self.normal_vector))
        return numpy.rad2deg(numpy.arctan2(self.col_vector.dot(multipath), self.row_vector.dot(multipath)))

    @property
    def GroundTrack(self):
        """
        float: The ground track angle.
        """

        track = self.ARPVel - (self.ARPVel.dot(self.normal_vector))*self.normal_vector
        return numpy.rad2deg(numpy.arctan2(self.col_vector.dot(track), self.row_vector.dot(track)))

    def get_ground_plane_resolution(self, row_ss, col_ss):
        """
        Get the resolution in the ground plane.

        Parameters
        ----------
        row_ss : float
        col_ss : float

        Returns
        -------
        float, float
        """

        x_g = self.slant_x - (self.slant_x.dot(self.normal_vector))*self.normal_vector
        theta_r = -numpy.arctan2(self.col_vector.dot(x_g), self.row_vector.dot(x_g))
        graze = numpy.deg2rad(self.GrazeAngle)
        tilt = numpy.deg2rad(self.TiltAngle)

        k_r1 = (numpy.cos(theta_r)/numpy.cos(graze))**2 + \
               (numpy.sin(theta_r)**2*numpy.tan(graze)*numpy.tan(tilt) -
                numpy.sin(2*theta_r)/numpy.cos(graze))*numpy.tan(graze)*numpy.tan(tilt)
        k_r2 = (numpy.sin(theta_r)/numpy.cos(tilt))**2

        k_c1 = (numpy.sin(theta_r)**2/numpy.cos(graze) -
                numpy.sin(2*theta_r)*numpy.tan(graze)*numpy.tan(tilt))/numpy.cos(graze) + \
               (numpy.cos(theta_r)*numpy.tan(graze)*numpy.tan(tilt))**2
        k_c2 = (numpy.cos(theta_r)/numpy.cos(tilt))**2

        r2 = row_ss*row_ss
        c2 = col_ss*col_ss
        return float(numpy.sqrt(k_r1*r2 + k_r2*c2)), float(numpy.sqrt(k_c1*r2 + k_c2*c2))

    @classmethod
    def from_sicd(cls, sicd, row_vector, col_vector):
        """
        Construct from SICD structure.

        Parameters
        ----------
        sicd : SICDType
        row_vector : numpy.ndarray
        col_vector : numpy.ndarray
        """

        calculator = GeometryCalculator(
            sicd.GeoData.SCP.ECF.get_array(),
            sicd.SCPCOA.ARPPos.get_array(),
            sicd.SCPCOA.ARPVel.get_array())
        return cls(calculator, row_vector, col_vector)


def _extract_sicd_tx_rcv_pol(str_in):
    """
    Extract the tx and rcv components from the sicd style tx/rcv polarization string.

    Parameters
    ----------
    str_in : str

    Returns
    -------
    str, str
    """

    if str_in is None:
        return 'UNKNOWN', 'UNKNOWN'

    if not isinstance(str_in, str):
        raise TypeError('requires a string type input.')

    if str_in in ['OTHER', 'UNKNOWN']:
        return str_in, str_in

    def _decode_single_pol(pol):
        if pol in RCV_POLARIZATION_VALUES:
            return pol
        if pol in ['X', 'Y', 'S', 'E'] or pol.startswith('OTHER'):
            return 'OTHER'
        return 'UNKNOWN'

    tx, rcv = str_in.split(':')
    tx = _decode_single_pol(tx)
    rcv = _decode_single_pol(rcv)

    return tx, rcv


class InputROIType(Serializable):
    """
    ROI representing portion of input data used to make this product.
    """

    _fields = ('Size', 'UpperLeft')
    _required = ('Size', 'UpperLeft')
    # Descriptor
    Size = SerializableDescriptor(
        'Size', RowColIntType, _required, strict=DEFAULT_STRICT,
        docstring='Number of rows and columns extracted from the input.')  # type: RowColIntType
    UpperLeft = SerializableDescriptor(
        'UpperLeft', RowColIntType, _required, strict=DEFAULT_STRICT,
        docstring='The upper-left pixel extracted from the input.')  # type: RowColIntType

    def __init__(self, Size=None, UpperLeft=None, **kwargs):
        """

        Parameters
        ----------
        Size : RowColIntType|numpy.ndarray|list|tuple
        UpperLeft : RowColIntType|numpy.ndarray|list|tuple
        kwargs
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.Size = Size
        self.UpperLeft = UpperLeft
        super(InputROIType, self).__init__(**kwargs)


class TxRcvPolarizationType(Serializable):
    """
    The transmit/receive polarization information.
    """

    _fields = ('TxPolarization', 'RcvPolarization', 'RcvPolarizationOffset')
    _required = ('TxPolarization', 'RcvPolarization')
    _numeric_format = {'RcvPolarizationOffset': FLOAT_FORMAT}
    # Descriptor
    TxPolarization = StringEnumDescriptor(
        'TxPolarization', TX_POLARIZATION_VALUES, _required, strict=DEFAULT_STRICT,
        docstring='Transmit polarization type.')  # type: str
    RcvPolarization = StringEnumDescriptor(
        'RcvPolarization', RCV_POLARIZATION_VALUES, _required, strict=DEFAULT_STRICT,
        docstring='Receive polarization type.')  # type: str
    RcvPolarizationOffset = FloatModularDescriptor(
        'RcvPolarizationOffset', 180.0, _required, strict=DEFAULT_STRICT,
        docstring='Angle offset for the receive polarization defined at aperture center.')  # type: float

    def __init__(self, TxPolarization=None, RcvPolarization=None, RcvPolarizationOffset=None, **kwargs):
        """

        Parameters
        ----------
        TxPolarization : str
        RcvPolarization : str
        RcvPolarizationOffset : None|float
        kwargs
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.TxPolarization = TxPolarization
        self.RcvPolarization = RcvPolarization
        self.RcvPolarizationOffset = RcvPolarizationOffset
        super(TxRcvPolarizationType, self).__init__(**kwargs)

    @classmethod
    def from_sicd_value(cls, str_in):
        """
        Construct from the sicd style tx/rcv polarization string.

        Parameters
        ----------
        str_in : str

        Returns
        -------
        TxRcvPolarizationType
        """

        tx, rcv = _extract_sicd_tx_rcv_pol(str_in)
        return cls(TxPolarization=tx, RcvPolarization=rcv)


class ProcTxRcvPolarizationType(Serializable):
    """
    The processed transmit/receive polarization.
    """
    _fields = ('TxPolarizationProc', 'RcvPolarizationProc')
    _required = ('TxPolarizationProc', 'RcvPolarizationProc')
    # Descriptor
    TxPolarizationProc = StringEnumDescriptor(
        'TxPolarizationProc', TX_POLARIZATION_VALUES, _required, strict=DEFAULT_STRICT,
        docstring='Transmit polarization type.')  # type: str
    RcvPolarizationProc = StringEnumDescriptor(
        'RcvPolarizationProc', RCV_POLARIZATION_VALUES, _required, strict=DEFAULT_STRICT,
        docstring='Receive polarization type.')  # type: str

    def __init__(self, TxPolarizationProc=None, RcvPolarizationProc=None, **kwargs):
        """

        Parameters
        ----------
        TxPolarizationProc : str
        RcvPolarizationProc : str
        kwargs
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.TxPolarizationProc = TxPolarizationProc
        self.RcvPolarizationProc = RcvPolarizationProc
        super(ProcTxRcvPolarizationType, self).__init__(**kwargs)

    @classmethod
    def from_sicd_value(cls, str_in):
        """
        Construct from the sicd style tx/rcv polarization string.

        Parameters
        ----------
        str_in : str

        Returns
        -------
        ProcTxRcvPolarizationType
        """

        tx, rcv = _extract_sicd_tx_rcv_pol(str_in)
        return cls(TxPolarizationProc=tx, RcvPolarizationProc=rcv)


class ExploitationFeaturesCollectionInformationType(Serializable):
    """
    General collection information.
    """

    _fields = (
        'SensorName', 'RadarMode', 'CollectionDateTime', 'LocalDateTime', 'CollectionDuration',
        'Resolution', 'InputROI', 'Polarizations')
    _required = ('SensorName', 'RadarMode', 'CollectionDateTime', 'CollectionDuration')
    _collections_tags = {'Polarizations': {'array': False, 'child_tag': 'Polarization'}}
    _numeric_format = {'CollectionDuration': FLOAT_FORMAT}
    # Descriptor
    SensorName = StringDescriptor(
        'SensorName', _required, strict=DEFAULT_STRICT,
        docstring='The name of the sensor.')  # str
    RadarMode = SerializableDescriptor(
        'RadarMode', RadarModeType, _required, strict=DEFAULT_STRICT,
        docstring='Radar collection mode.')  # type: RadarModeType
    CollectionDateTime = DateTimeDescriptor(
        'CollectionDateTime', _required, strict=DEFAULT_STRICT,
        docstring='Collection date and time defined in Coordinated Universal Time (UTC). The seconds '
                  'should be followed by a Z to indicate UTC.')  # type: numpy.datetime64
    CollectionDuration = FloatDescriptor(
        'CollectionDuration', _required, strict=DEFAULT_STRICT,
        docstring='The duration of the collection (units = seconds).')  # type: float
    Resolution = SerializableDescriptor(
        'Resolution', RangeAzimuthType, _required, strict=DEFAULT_STRICT,
        docstring='Uniformly-weighted resolution (range and azimuth) processed in '
                  'the slant plane.')  # type: Union[None, RangeAzimuthType]
    InputROI = SerializableDescriptor(
        'InputROI', InputROIType, _required, strict=DEFAULT_STRICT,
        docstring='ROI representing portion of input data used to make '
                  'this product.')  # type: Union[None, InputROIType]
    Polarizations = SerializableListDescriptor(
        'Polarizations', TxRcvPolarizationType, _collections_tags, _required, strict=DEFAULT_STRICT,
        docstring='Transmit and receive polarization(s).')  # type: Union[None, List[TxRcvPolarizationType]]

    def __init__(self, SensorName=None, RadarMode=None, CollectionDateTime=None, LocalDateTime=None,
                 CollectionDuration=None, Resolution=None, Polarizations=None, **kwargs):
        """

        Parameters
        ----------
        SensorName : str
        RadarMode : RadarModeType
        CollectionDateTime : numpy.datetime64|datetime.datetime|datetime.date|str
        LocalDateTime : None|str|datetime.datetime
        CollectionDuration : float
        Resolution : None|RangeAzimuthType|numpy.ndarray|list|tuple
        Polarizations : None|List[TxRcvPolarizationType]
        kwargs
        """

        self._local_date_time = None
        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.SensorName = SensorName
        self.RadarMode = RadarMode
        self.CollectionDateTime = CollectionDateTime
        self.CollectionDuration = CollectionDuration
        self.LocalDateTime = LocalDateTime
        self.Resolution = Resolution
        self.Polarizations = Polarizations
        super(ExploitationFeaturesCollectionInformationType, self).__init__(**kwargs)

    @property
    def LocalDateTime(self):
        """None|str:  The local date/time string of the collection. *Optional.*"""
        return self._local_date_time

    @LocalDateTime.setter
    def LocalDateTime(self, value):  # type: (Union[None, str, datetime.datetime]) -> None
        if value is None:
            self._local_date_time = None
            return
        elif isinstance(value, datetime.datetime):
            value = value.isoformat('T')

        if isinstance(value, str):
            self._local_date_time = value
        else:
            logger.error(
                'Attribute LocalDateTime of class ExploitationFeaturesCollectionInformationType\n\t'
                'requires a datetime.datetime or string. Got unsupported type {}.\n\t'
                'Setting value to None.'.format(type(value)))
            self._local_date_time = None

    @classmethod
    def from_sicd(cls, sicd):
        """
        Construct from a sicd element.

        Parameters
        ----------
        sicd : SICDType

        Returns
        -------
        ExploitationFeaturesCollectionInformationType
        """

        if not isinstance(sicd, SICDType):
            raise TypeError(_sicd_type_text.format(type(sicd)))

        polarizations = [
            TxRcvPolarizationType.from_sicd_value(entry.TxRcvPolarization)
            for entry in sicd.RadarCollection.RcvChannels]

        return cls(SensorName=sicd.CollectionInfo.CollectorName,
                   RadarMode=RadarModeType(**sicd.CollectionInfo.RadarMode.to_dict()),
                   CollectionDateTime=sicd.Timeline.CollectStart,
                   CollectionDuration=sicd.Timeline.CollectDuration,
                   Resolution=get_collection_resolution(sicd),
                   Polarizations=polarizations)


class ExploitationFeaturesCollectionGeometryType(Serializable):
    """
    Key geometry parameters independent of product processing. All values
    computed at the center time of the full collection.
    """

    _fields = ('Azimuth', 'Slope', 'Squint', 'Graze', 'Tilt', 'DopplerConeAngle', 'Extensions')
    _required = ()
    _collections_tags = {'Extensions': {'array': False, 'child_tag': 'Extension'}}
    _numeric_format = {
        'Azimuth': FLOAT_FORMAT, 'Slope': FLOAT_FORMAT, 'Squint': FLOAT_FORMAT, 'Graze': FLOAT_FORMAT,
        'Tilt': FLOAT_FORMAT, 'DopplerConeAngle': FLOAT_FORMAT}
    # Descriptor
    Azimuth = FloatDescriptor(
        'Azimuth', _required, strict=DEFAULT_STRICT, bounds=(0.0, 360.0),
        docstring='Angle clockwise from north indicating the ETP line of sight vector.')  # type: float
    Slope = FloatDescriptor(
        'Slope', _required, strict=DEFAULT_STRICT, bounds=(0.0, 90.0),
        docstring='Angle between the ETP at scene center and the range vector perpendicular to '
                  'the direction of motion.')  # type: float
    Squint = FloatModularDescriptor(
        'Squint', 180.0, _required, strict=DEFAULT_STRICT,
        docstring='Angle from the ground track to platform velocity vector at nadir. '
                  'Left-look is positive, right-look is negative.')  # type: float
    Graze = FloatDescriptor(
        'Graze', _required, strict=DEFAULT_STRICT, bounds=(0.0, 90.0),
        docstring='Angle between the ETP and the line of sight vector.')  # type: float
    Tilt = FloatModularDescriptor(
        'Tilt', 180.0, _required, strict=DEFAULT_STRICT,
        docstring='Angle between the ETP and the cross range vector. '
                  'Also known as the twist angle.')  # type: float
    DopplerConeAngle = FloatDescriptor(
        'DopplerConeAngle', _required, strict=DEFAULT_STRICT, bounds=(0.0, 180.0),
        docstring='The angle between the velocity vector and the radar line-of-sight vector. '
                  'Also known as the slant plane squint angle.')  # type: float
    Extensions = ParametersDescriptor(
        'Extensions', _collections_tags, _required, strict=DEFAULT_STRICT,
        docstring='Exploitation feature extension related to geometry for a '
                  'single input image.')  # type: ParametersCollection

    def __init__(self, Azimuth=None, Slope=None, Squint=None, Graze=None, Tilt=None,
                 DopplerConeAngle=None, Extensions=None, **kwargs):
        """

        Parameters
        ----------
        Azimuth : None|float
        Slope : None|float
        Squint : None|float
        Graze : None|float
        Tilt : None|float
        DopplerConeAngle : None|float
        Extensions : None|ParametersCollection|dict
        kwargs
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.Azimuth = Azimuth
        self.Slope = Slope
        self.Squint = Squint
        self.Graze = Graze
        self.Tilt = Tilt
        self.DopplerConeAngle = DopplerConeAngle
        self.Extensions = Extensions
        super(ExploitationFeaturesCollectionGeometryType, self).__init__(**kwargs)

    @classmethod
    def from_calculator(cls, calculator):
        """
        Create from an ExploitationCalculator object.

        Parameters
        ----------
        calculator : ExploitationCalculator

        Returns
        -------
        ExploitationFeaturesCollectionGeometryType
        """

        if not isinstance(calculator, ExploitationCalculator):
            raise TypeError(_exp_calc_text.format(type(calculator)))

        return cls(Azimuth=calculator.AzimuthAngle,
                   Slope=calculator.SlopeAngle,
                   Graze=calculator.GrazeAngle,
                   Tilt=calculator.TiltAngle,
                   DopplerConeAngle=calculator.DopplerConeAngle,
                   Squint=calculator.SquintAngle)


class ExploitationFeaturesCollectionPhenomenologyType(Serializable):
    """
    Phenomenology related to both the geometry and the final product processing.
    All values computed at the center time of the full collection.
    """

    _fields = ('Shadow', 'Layover', 'MultiPath', 'GroundTrack', 'Extensions')
    _required = ()
    _collections_tags = {'Extensions': {'array': False, 'child_tag': 'Extension'}}
    _numeric_format = {'MultiPath': FLOAT_FORMAT, 'GroundTrack': FLOAT_FORMAT}
    # Descriptor
    Shadow = SerializableDescriptor(
        'Shadow', AngleMagnitudeType, _required, strict=DEFAULT_STRICT,
        docstring='The phenomenon where vertical objects occlude radar '
                  'energy.')  # type: Union[None, AngleMagnitudeType]
    Layover = SerializableDescriptor(
        'Layover', AngleMagnitudeType, _required, strict=DEFAULT_STRICT,
        docstring='The phenomenon where vertical objects appear as ground objects with '
                  'the same range/range rate.')  # type: Union[None, AngleMagnitudeType]
    MultiPath = FloatModularDescriptor(
        'MultiPath', 180.0, _required, strict=DEFAULT_STRICT,
        docstring='This is a range dependent phenomenon which describes the energy from a '
                  'single scatter returned to the radar via more than one path and results '
                  'in a nominally constant direction in the ETP.')  # type: Union[None, float]
    GroundTrack = FloatModularDescriptor(
        'GroundTrack', 180.0, _required, strict=DEFAULT_STRICT,
        docstring='Counter-clockwise angle from increasing row direction to ground track '
                  'at the center of the image.')  # type: Union[None, float]
    Extensions = ParametersDescriptor(
        'Extensions', _collections_tags, _required, strict=DEFAULT_STRICT,
        docstring='Exploitation feature extension related to geometry for a '
                  'single input image.')  # type: ParametersCollection

    def __init__(self, Shadow=None, Layover=None, MultiPath=None, GroundTrack=None, Extensions=None, **kwargs):
        """

        Parameters
        ----------
        Shadow : None|AngleMagnitudeType|numpy.ndarray|list|tuple
        Layover : None|AngleMagnitudeType|numpy.ndarray|list|tuple
        MultiPath : None|float
        GroundTrack : None|float
        Extensions : None|ParametersCollection|dict
        kwargs
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.Shadow = Shadow
        self.Layover = Layover
        self.MultiPath = MultiPath
        self.GroundTrack = GroundTrack
        self.Extensions = Extensions
        super(ExploitationFeaturesCollectionPhenomenologyType, self).__init__(**kwargs)

    @classmethod
    def from_calculator(cls, calculator):
        """
        Create from an ExploitationCalculator object.

        Parameters
        ----------
        calculator : ExploitationCalculator

        Returns
        -------
        ExploitationFeaturesCollectionPhenomenologyType
        """

        if not isinstance(calculator, ExploitationCalculator):
            raise TypeError(_exp_calc_text.format(type(calculator)))
        return cls(Shadow=calculator.Shadow, Layover=calculator.Layover,
                   MultiPath=calculator.MultiPath, GroundTrack=calculator.GroundTrack)


class CollectionType(Serializable):
    """
    Metadata regarding one of the input collections.
    """
    _fields = ('Information', 'Geometry', 'Phenomenology', 'identifier')
    _required = ('Information', 'identifier')
    _set_as_attribute = ('identifier', )
    # Descriptor
    Information = SerializableDescriptor(
        'Information', ExploitationFeaturesCollectionInformationType, _required, strict=DEFAULT_STRICT,
        docstring='General collection information.')  # type: ExploitationFeaturesCollectionInformationType
    Geometry = SerializableDescriptor(
        'Geometry', ExploitationFeaturesCollectionGeometryType, _required, strict=DEFAULT_STRICT,
        docstring='Key geometry parameters independent of product '
                  'processing.')  # type: Union[None, ExploitationFeaturesCollectionGeometryType]
    Phenomenology = SerializableDescriptor(
        'Phenomenology', ExploitationFeaturesCollectionPhenomenologyType, _required, strict=DEFAULT_STRICT,
        docstring='Phenomenology related to both the geometry and the final '
                  'product processing.')  # type: Union[None, ExploitationFeaturesCollectionPhenomenologyType]
    identifier = StringDescriptor(
        'identifier', _required, strict=DEFAULT_STRICT,
        docstring='The exploitation feature identifier.')  # type: str

    def __init__(self, Information=None, Geometry=None, Phenomenology=None, identifier=None, **kwargs):
        """

        Parameters
        ----------
        Information : ExploitationFeaturesCollectionInformationType
        Geometry : None|ExploitationFeaturesCollectionGeometryType
        Phenomenology : None|ExploitationFeaturesCollectionPhenomenologyType
        identifier : str
        kwargs
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.Information = Information
        self.Geometry = Geometry
        self.Phenomenology = Phenomenology
        self.identifier = identifier
        super(CollectionType, self).__init__(**kwargs)

    @classmethod
    def from_calculator(cls, calculator, sicd):
        """
        Create from an ExploitationCalculator object.

        Parameters
        ----------
        calculator : ExploitationCalculator
        sicd : SICDType

        Returns
        -------
        CollectionType
        """

        if not isinstance(calculator, ExploitationCalculator):
            raise TypeError(_exp_calc_text.format(type(calculator)))

        return cls(identifier=sicd.CollectionInfo.CoreName,
                   Information=ExploitationFeaturesCollectionInformationType.from_sicd(sicd),
                   Geometry=ExploitationFeaturesCollectionGeometryType.from_calculator(calculator),
                   Phenomenology=ExploitationFeaturesCollectionPhenomenologyType.from_calculator(calculator))


class ExploitationFeaturesProductType(Serializable):
    """
    Metadata regarding the product.
    """

    _fields = ('Resolution', 'Ellipticity', 'Polarizations', 'North', 'Extensions')
    _required = ('Resolution', 'Ellipticity', 'Polarizations')
    _collections_tags = {
        'Polarizations': {'array': False, 'child_tag': 'Polarization'},
        'Extensions': {'array': False, 'child_tag': 'Extension'}}
    _numeric_format = {'Ellipticity': FLOAT_FORMAT, 'North': FLOAT_FORMAT}
    # Descriptor
    Resolution = SerializableDescriptor(
        'Resolution', RowColDoubleType, _required, strict=DEFAULT_STRICT,
        docstring='Uniformly-weighted resolution projected into the Earth Tangent '
                  'Plane (ETP).')  # type: RowColDoubleType
    Ellipticity = FloatDescriptor(
        'Ellipticity', _required, strict=DEFAULT_STRICT,
        docstring="Ellipticity of the 2D-IPR at the ORP, measured in the *Earth Geodetic "
                  "Tangent Plane (EGTP)*. Ellipticity is the ratio of the IPR ellipse's "
                  "major axis to minor axis.")  # type: float
    Polarizations = SerializableListDescriptor(
        'Polarizations', ProcTxRcvPolarizationType, _collections_tags, _required, strict=DEFAULT_STRICT,
        docstring='Describes the processed transmit and receive polarizations for the '
                  'product.')  # type: List[ProcTxRcvPolarizationType]
    North = FloatModularDescriptor(
        'North', 180.0, _required, strict=DEFAULT_STRICT,
        docstring='Counter-clockwise angle from increasing row direction to north at the center '
                  'of the image.')  # type: float
    Extensions = ParametersDescriptor(
        'Extensions', _collections_tags, _required, strict=DEFAULT_STRICT,
        docstring='Exploitation feature extension related to geometry for a '
                  'single input image.')  # type: ParametersCollection

    def __init__(self, Resolution=None, Ellipticity=None, Polarizations=None,
                 North=None, Extensions=None, **kwargs):
        """

        Parameters
        ----------
        Resolution : RowColDoubleType|numpy.ndarray|list|tuple
        Ellipticity : float
        Polarizations : List[ProcTxRcvPolarizationType]
        North : None|float
        Extensions : None|ParametersCollection|dict
        kwargs
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.Resolution = Resolution
        self.Ellipticity = Ellipticity
        self.Polarizations = Polarizations
        self.North = North
        self.Extensions = Extensions
        super(ExploitationFeaturesProductType, self).__init__(**kwargs)

    @classmethod
    def from_calculator(cls, calculator, sicd):
        """
        Construct from a sicd element.

        Parameters
        ----------
        calculator : ExploitationCalculator
        sicd : SICDType

        Returns
        -------
        ExploitationFeaturesProductType
        """

        if not isinstance(sicd, SICDType):
            raise TypeError(_sicd_type_text.format(type(sicd)))

        rho_rg_slant, rho_az_slant = get_collection_resolution(sicd)
        rho_row_etp, rho_col_etp = calculator.get_ground_plane_resolution(rho_rg_slant, rho_az_slant)
        ellipticity = rho_row_etp/rho_col_etp if rho_row_etp >= rho_col_etp else rho_col_etp/rho_row_etp

        return cls(Resolution=(rho_row_etp, rho_col_etp),
                   Ellipticity=ellipticity,
                   Polarizations=[
                       ProcTxRcvPolarizationType.from_sicd_value(sicd.ImageFormation.TxRcvPolarizationProc), ],
                   North=calculator.North)


class ExploitationFeaturesType(Serializable):
    """
    Computed metadata regarding the collect.
    """
    _fields = ('Collections', 'Products')
    _required = ('Collections', 'Products')
    _collections_tags = {
        'Collections': {'array': False, 'child_tag': 'Collection'},
        'Products': {'array': False, 'child_tag': 'Product'}}
    # Descriptor
    Collections = SerializableListDescriptor(
        'Collections', CollectionType, _collections_tags, _required, strict=DEFAULT_STRICT,
        docstring='')  # type: List[CollectionType]
    Products = SerializableListDescriptor(
        'Products', ExploitationFeaturesProductType, _collections_tags, _required, strict=DEFAULT_STRICT,
        docstring='')  # type: List[ExploitationFeaturesProductType]

    def __init__(self, Collections=None, Products=None, **kwargs):
        """

        Parameters
        ----------
        Collections : List[CollectionType]
        Products : List[ExploitationFeaturesProductType]
        kwargs
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.Collections = Collections
        self.Products = Products
        super(ExploitationFeaturesType, self).__init__(**kwargs)

    @classmethod
    def from_sicd(cls, sicd, row_vector, col_vector):
        """
        Construct from a sicd element.

        Parameters
        ----------
        sicd : SICDType|List[SICDType]
        row_vector : numpy.ndarray
        col_vector : numpy.ndarray

        Returns
        -------
        ExploitationFeaturesType
        """
        if isinstance(sicd, (list, tuple)):
            collections = []
            feats = []
            for i, entry in sicd:
                calculator = ExploitationCalculator.from_sicd(entry, row_vector, col_vector)
                collections.append(CollectionType.from_calculator(calculator, entry))
                feats.append(ExploitationFeaturesProductType.from_calculator(calculator, entry))
            return cls(Collections=collections, Products=feats)

        if not isinstance(sicd, SICDType):
            raise TypeError(_sicd_type_text.format(type(sicd)))
        calculator = ExploitationCalculator.from_sicd(sicd, row_vector, col_vector)
        return cls(
            Collections=[CollectionType.from_calculator(calculator, sicd), ],
            Products=[ExploitationFeaturesProductType.from_calculator(calculator, sicd)])


def get_collection_resolution(sicd_meta):
    """Compute the uniformly-weighted resolution (half-power width) in the slant plane from SICD metadata"""
    hpbw_rectangular = sarpy.processing.sicd.windows.find_half_power(numpy.ones(2**10))

    delta_xrow = hpbw_rectangular / sicd_meta.Grid.Row.ImpRespBW
    delta_ycol = hpbw_rectangular / sicd_meta.Grid.Col.ImpRespBW
    m_spxy_il = sarpy.geometry.point_projection.image_to_slant_sensitivity(sicd_meta, delta_xrow, delta_ycol)

    delta_spx, delta_spy = m_spxy_il * [delta_xrow, delta_ycol]
    return max(abs(delta_spx)), max(abs(delta_spy))
