"""
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, StringRegexDescriptor, \
    DateTimeDescriptor, SerializableListDescriptor
from sarpy.io.complex.sicd_elements.SICD import SICDType
from sarpy.geometry.geocoords import ecf_to_geodetic

from .base import DEFAULT_STRICT, FLOAT_FORMAT
from sarpy.io.product.sidd2_elements.ExploitationFeatures import ExploitationCalculator as BaseExploitationCalculator
from .blocks import AngleZeroToExclusive360MagnitudeType, RadarModeType, RangeAzimuthType, RowColDoubleType

from sarpy.io.product.sidd2_elements.ExploitationFeatures import (
    InputROIType,
    get_collection_resolution,
)

__REUSED__ = (
    InputROIType,
)


RCV_POLARIZATION_PATTERN = 'V|H|X|Y|S|E|RHC|LHC|UNKNOWN|OTHER[^:]*'
TX_POLARIZATION_PATTERN = RCV_POLARIZATION_PATTERN + '|SEQUENCE'
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(BaseExploitationCalculator):
    @property
    def Shadow(self):
        # type: () -> AngleZeroToExclusive360MagnitudeType
        """
        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
        theta_shadow = numpy.rad2deg(numpy.arctan2(self.row_vector.dot(shadow_prime), self.col_vector.dot(shadow_prime)))
        if theta_shadow < 0:
            theta_shadow += 360.0

        return AngleZeroToExclusive360MagnitudeType(
            Angle=theta_shadow,
            Magnitude=numpy.linalg.norm(shadow_prime))

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

        layover = self.normal_vector - self.slant_z/(self.slant_z.dot(self.normal_vector))
        theta_layover = numpy.rad2deg(numpy.arctan2(self.row_vector.dot(layover), self.col_vector.dot(layover)))
        if theta_layover < 0:
            theta_layover += 360.0

        return AngleZeroToExclusive360MagnitudeType(
            Angle=theta_layover,
            Magnitude=numpy.linalg.norm(layover))

    @property
    def North(self):
        """Describes the clockwise angle from increasing column direction to north at the center of
        the image.  Angle range: [0, 360)

        Returns
        -------
        float: The north angle.
        """

        lat, lon, _ = 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))
        theta_north = numpy.rad2deg(numpy.arctan2(self.row_vector.dot(north_prime), self.col_vector.dot(north_prime)))

        if theta_north < 0:
            theta_north = theta_north + 360.0
        return theta_north

    @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))
        theta_multipath = numpy.rad2deg(numpy.arctan2(self.row_vector.dot(multipath), self.col_vector.dot(multipath)))
        if theta_multipath < 0:
            theta_multipath += 360.0
        return theta_multipath

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

        track = self.ARPVel - (self.ARPVel.dot(self.normal_vector))*self.normal_vector
        theta_track = numpy.rad2deg(numpy.arctan2(self.row_vector.dot(track), self.col_vector.dot(track)))
        if theta_track < 0:
            theta_track += 360.0
        return theta_track


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

    return str_in.split(':')


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

    _fields = ('TxPolarization', 'RcvPolarization', 'RcvPolarizationOffset')
    _required = ('TxPolarization', 'RcvPolarization')
    _numeric_format = {'RcvPolarizationOffset': FLOAT_FORMAT}
    # Descriptor
    TxPolarization = StringRegexDescriptor(
        'TxPolarization', TX_POLARIZATION_PATTERN, _required, strict=DEFAULT_STRICT,
        docstring='Transmit polarization type.')  # type: str
    RcvPolarization = StringRegexDescriptor(
        'RcvPolarization', RCV_POLARIZATION_PATTERN, _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 = StringRegexDescriptor(
        'TxPolarizationProc', TX_POLARIZATION_PATTERN, _required, strict=DEFAULT_STRICT,
        docstring='Transmit polarization type.')  # type: str
    RcvPolarizationProc = StringRegexDescriptor(
        'RcvPolarizationProc', RCV_POLARIZATION_PATTERN, _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', AngleZeroToExclusive360MagnitudeType, _required, strict=DEFAULT_STRICT,
        docstring='The phenomenon where vertical objects occlude radar '
                  'energy.')  # type: Union[None, AngleZeroToExclusive360MagnitudeType]
    Layover = SerializableDescriptor(
        'Layover', AngleZeroToExclusive360MagnitudeType, _required, strict=DEFAULT_STRICT,
        docstring='The phenomenon where vertical objects appear as ground objects with '
                  'the same range/range rate.')  # type: Union[None, AngleZeroToExclusive360MagnitudeType]
    MultiPath = FloatDescriptor(
        'MultiPath', _required, strict=DEFAULT_STRICT, bounds=(0.0, 360.0),
        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 = FloatDescriptor(
        'GroundTrack', _required, strict=DEFAULT_STRICT, bounds=(0.0, 360.0),
        docstring='Counter-clockwise angle from increasing column 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|AngleZeroToExclusive360MagnitudeType|numpy.ndarray|list|tuple
        Layover : None|AngleZeroToExclusive360MagnitudeType|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 = FloatDescriptor(
        'North', _required, strict=DEFAULT_STRICT, bounds=(0.0, 360.0),
        docstring='Clockwise angle from increasing column 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)])
