"""
The ExploitationFeaturesType definition for SIDD 1.0
"""

__classification__ = "UNCLASSIFIED"
__author__ = "Thomas McCullough"

import logging
import datetime
from typing import Union, List

import numpy

from sarpy.io.product.sidd2_elements.base import DEFAULT_STRICT, FLOAT_FORMAT

from sarpy.io.xml.base import Serializable, ParametersCollection
from sarpy.io.xml.descriptors import SerializableDescriptor, SerializableListDescriptor, \
    FloatDescriptor, FloatModularDescriptor, StringDescriptor, StringEnumDescriptor, \
    DateTimeDescriptor, ParametersDescriptor, BooleanDescriptor

from sarpy.io.product.sidd2_elements.blocks import RowColDoubleType, RangeAzimuthType, RadarModeType
# noinspection PyProtectedMember
from sarpy.io.product.sidd2_elements.ExploitationFeatures import InputROIType, \
    ExploitationFeaturesCollectionPhenomenologyType, \
    ExploitationCalculator, get_collection_resolution
from sarpy.io.complex.sicd_elements.SICD import SICDType

POLARIZATION_VALUES = ('V', 'H', 'RHC', 'LHC', 'OTHER')

logger = logging.getLogger(__name__)

_sicd_type_text = 'Requires instance of SICDType,\n\tgot type `{}`'


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 'OTHER', 'OTHER'

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

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

    def _decode_single_pol(pol):
        if pol in POLARIZATION_VALUES:
            return pol
        return 'OTHER'

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

    return tx, rcv


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

    _fields = ('TxPolarization', 'RcvPolarization', 'RcvPolarizationOffset', 'Processed')
    _required = ('TxPolarization', 'RcvPolarization')
    _numeric_format = {'RcvPolarizationOffset': FLOAT_FORMAT}
    # Descriptor
    TxPolarization = StringEnumDescriptor(
        'TxPolarization', POLARIZATION_VALUES, _required, strict=DEFAULT_STRICT,
        docstring='Transmit polarization type.')  # type: str
    RcvPolarization = StringEnumDescriptor(
        'RcvPolarization', 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: Union[None, float]
    Processed = BooleanDescriptor(
        'Processed', _required, strict=DEFAULT_STRICT,
        docstring='')  # type: Union[None, bool]

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

        Parameters
        ----------
        TxPolarization : str
        RcvPolarization : str
        RcvPolarizationOffset : None|float
        Processed : None|bool
        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
        self.Processed = Processed
        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 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[TxTcvPolarizationType]
        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', '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}
    # 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
    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,
                 Extensions=None, **kwargs):
        """

        Parameters
        ----------
        Azimuth : None|float
        Slope : None|float
        Squint : None|float
        Graze : None|float
        Tilt : 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.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(
                'Requires input which is an instance of ExploitationCalculator, got type {}'.format(type(calculator)))

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


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(
                'Requires input which is an instance of ExploitationCalculator, got type {}'.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', 'North', 'Extensions')
    _required = ('Resolution', )
    _collections_tags = {
        'Extensions': {'array': False, 'child_tag': 'Extension'}}
    _numeric_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
    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, North=None, Extensions=None, **kwargs):
        """

        Parameters
        ----------
        Resolution : RowColDoubleType|numpy.ndarray|list|tuple
        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.North = North
        self.Extensions = Extensions
        super(ExploitationFeaturesProductType, self).__init__(**kwargs)

    @classmethod
    def from_sicd(cls, sicd):
        """

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

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

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

        row_ground, col_ground = sicd.get_ground_resolution()
        return cls(Resolution=(row_ground, col_ground))

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

        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)

        return cls(Resolution=(rho_row_etp, rho_col_etp),
                   North=calculator.North)


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

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

        Parameters
        ----------
        Collections : List[CollectionType]
        Product : 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.Product = Product
        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)):
            return cls(
                Collections=[
                    CollectionType.from_calculator(
                        ExploitationCalculator.from_sicd(entry, row_vector, col_vector), entry) for entry in sicd],
                Product=ExploitationFeaturesProductType.from_calculator(
                    ExploitationCalculator.from_sicd(sicd[0], row_vector, col_vector),
                    sicd[0]))

        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), ],
            Product=ExploitationFeaturesProductType.from_calculator(calculator, sicd))
