# Copyright (c) 2014, The MITRE Corporation. All rights reserved.
# See LICENSE.txt for complete terms.

import stix
import stix.utils
from stix.utils import dates

import stix.bindings.exploit_target as exploit_target_binding
from stix.common.related import (GenericRelationshipList, RelatedCOA, 
                                RelatedExploitTarget, RelatedPackageRefs)
from stix.common import StructuredText, VocabString, InformationSource
from stix.coa import CourseOfAction
from stix.data_marking import Marking
from .vulnerability import Vulnerability
from .weakness import Weakness
from .configuration import Configuration

from datetime import datetime
from dateutil.tz import tzutc

class ExploitTarget(stix.Entity):
    """Implementation of STIX ``ExploitTarget``.

    Args:
        id_ (optional): An identifier. If ``None``, a value will be generated
            via ``stix.utils.create_id()``. If set, this will unset the
            ``idref`` property.
        idref (optional): An identifier reference. If set this will unset the
            ``id_`` property.
        title (optional): A string title.
        timestamp (optional): A timestamp value. Can be an instance of
            ``datetime.datetime`` or ``str``.
        description (optional): A string description.
        short_description (optional): A string short description.

    """
        
    _binding = exploit_target_binding
    _binding_class = _binding.ExploitTargetType
    _namespace = "http://stix.mitre.org/ExploitTarget-1"
    _version = "1.1.1"
    _ALL_VERSIONS = ("1.0", "1.0.1", "1.1", "1.1.1")

    def __init__(self, id_=None, idref=None, timestamp=None, title=None, description=None, short_description=None):
        self.id_ = id_ or stix.utils.create_id("et")
        self.idref = idref
        self.version = None
        self.title = title
        self.description = description
        self.short_description = short_description
        self.information_source = None
        self.handling = None
        self.potential_coas = PotentialCOAs()
        self.related_exploit_targets = RelatedExploitTargets()
        self.vulnerabilities = None
        self.weaknesses = None
        self.configuration = None
        self.related_packages = RelatedPackageRefs()
        
        if timestamp:
            self.timestamp = timestamp
        else:
            self.timestamp = datetime.now(tzutc()) if not idref else None
            
    @property
    def id_(self):
        """The ``id_`` property for this :class:`ExploitTarget` which serves as
        an identifier. This is automatically set during ``__init__()``.

        Default Value: ``None``

        Note:
            The :class:`ExploitTarget` class cannot have both its ``id_`` and
            ``idref`` properties set at the same time. As such, setting the
            ``idref`` property will unset the ``id_`` property and setting
            the ``id_`` property will unset the ``idref`` property.

        Returns:
            A string id.

        """
        return self._id
    
    @id_.setter
    def id_(self, value):
        if not value:
            self._id = None
        else:
            self._id = value
            self.idref = None
    
    @property
    def version(self):
        """The ``version`` property of this :class:`ExploitTarget`. This property
        will always return ``None`` unless it is set to a value different than
        ``ExploitTarget._version``.

        Note:
            This property refers to the version of the ``ExploitTarget`` schema
            type and should not be used for the purpose of content versioning.

        Default Value: ``None``

        Returns:
            The value of the ``version`` property if set to a value different
            than ``ExploitTarget._version``

        """
        
        return self._version
    
    @version.setter
    def version(self, value):
        if not value:
            self._version = None
        else:
            if value not in self._ALL_VERSIONS:
                msg = ("Version must be one of %s. Found '%s'" %
                      (self._ALL_VERSIONS, value))
                raise ValueError(msg)
            self._version = value
    
    @property
    def idref(self):
        """The ``idref`` property for this :class:`ExploitTarget`.

        The ``idref`` property must be set to the ``id_`` value of another
        :class:`ExploitTarget` instance. An idref does not need to resolve to a
        local :class:`ExploitTarget` instance.

        Default Value: ``None``.

        Note:
            The :class:`ExploitTarget` class cannot have both its ``id_`` and
            ``idref`` properties set at the same time. As such, setting the
            ``idref`` property will unset the ``id_`` property and setting
            the ``id_`` property will unset the ``idref`` property.

        Returns:
            The value of the ``idref`` property

        """
        return self._idref
    
    @idref.setter
    def idref(self, value):
        if not value:
            self._idref = None
        else:
            self._idref = value
            self.id_ = None # unset id_ if idref is present
    
    @property
    def timestamp(self):
        """The ``timestamp`` propety for this :class:`ExploitTarget` instance. This
        property declares the time of creation and is automatically set in
        ``__init__()``.

        This property can accept ``datetime.datetime`` or ``str`` values.
        If an ``str`` value is supplied, a best-effort attempt is made to
        parse it into an instance of ``datetime.datetime``.

        Default Value: A ``datetime.dateime`` instance with a value of the
        date/time when ``__init__()`` was called.

        Note:
            If an ``idref`` is set during ``__init__()``, the value of
            ``timestamp`` will not automatically generated and instead default
            to the ``timestamp`` parameter, which has a default value of
            ``None``.

        Returns:
            An instance of ``datetime.datetime``.

        """
        
        return self._timestamp

    @timestamp.setter
    def timestamp(self, value):
        self._timestamp = dates.parse_value(value)

    @property
    def title(self):
        return self._title

    @title.setter
    def title(self, value):
        self._title = value

    @property
    def description(self):
        return self._description

    @description.setter
    def description(self, value):
        """The ``description`` property for this :class:`ExploitTarget`.

        Default Value: ``None``

        Note:
            If set to a value that is not an instance of
            :class:`stix.common.structured_text.StructuredText`, an attempt to
            will be made to convert the value into an instance of
            :class:`stix.common.structured_text.StructuredText`.

        Returns:
            An instance of
            :class:`stix.common.structured_text.StructuredText`

        """
        if value:
            if isinstance(value, StructuredText):
                self._description = value
            else:
                self._description = StructuredText(value=value)
        else:
            self._description = None

    @property
    def short_description(self):
        """The ``short_description`` property for this :class:`ExploitTarget`.

        Default Value: ``None``

        Note:
            If set to a value that is not an instance of
            :class:`stix.common.structured_text.StructuredText`, an attempt to
            will be made to convert the value into an instance of
            :class:`stix.common.structured_text.StructuredText`.

        Returns:
            An instance of
            :class:`stix.common.structured_text.StructuredText`

        """

        return self._short_description

    @short_description.setter
    def short_description(self, value):
        if value:
            if isinstance(value, StructuredText):
                self._short_description = value
            else:
                self._short_description = StructuredText(value=value)
        else:
            self._short_description = None

    @property
    def information_source(self):
        """Contains information about the source of the :class:`ExploitTarget`.

        Default Value: ``None``

        Returns:
            An instance of
            :class:`stix.common.information_source.InformationSource`

        Raises:
            ValueError: If set to a value that is not ``None`` and not an
                instance of
                :class:`stix.common.information_source.InformationSource`

        """
        
        return self._information_source
    
    @information_source.setter
    def information_source(self, value):
        if not value:
            self._information_source = None
        elif isinstance(value, InformationSource):
            self._information_source = value
        else:
            raise ValueError('value must be instance of InformationSource')

    @property
    def handling(self):
        
        return self._handling

    @handling.setter
    def handling(self, value):
        if value and not isinstance(value, Marking):
            raise ValueError('value must be instance of Marking')

        self._handling = value
 
    @property
    def vulnerabilities(self):
        """A list of ``Vulnerability`` objects 
        
        Default Value: ``None``

        Returns:
            A list of
            :class:`stix.exploit_target.vulnerability`

        Raises:
            ValueError: If set to a value that is not ``None`` and not an
                instance of
                :class:`stix.exploit_target.vulnerability`

        """
        
        return self._vulnerabilities
    
    @vulnerabilities.setter
    def vulnerabilities(self, value):
        self._vulnerabilities = []
        if not value:
            return
        elif isinstance(value, list):
            for v in value:
                self.add_vulnerability(v)
        else:
            self.add_vulnerability(value)
            
    def add_vulnerability(self, v):
        """Adds a vulnerability to the ``vulnerabilies`` list property.

        Note:
            If ``None`` is passed in no value is added 

        Args:
            v: A Vulnerability value.
            
        Raises: ValueError if the ``v`` param is of type :class:`stix.exploit_target.vulnerability`
    

        """
        if not v:
            return
        elif isinstance(v, Vulnerability):
            self.vulnerabilities.append(v)
        else:
            raise ValueError('Cannot add type %s to vulnerabilities list' % type(v))
 
    @property
    def weaknesses(self):
        """A list of ``Weakness`` objects 
        
        Default Value: ``None``

        Returns:
            A list of
            :class:`stix.exploit_target.weakness`

        Raises:
            ValueError: If set to a value that is not ``None`` and not an
                instance of
                :class:`stix.exploit_target.weakness`

        """
        
        return self._weaknesses
    
    @weaknesses.setter
    def weaknesses(self, value):
        self._weaknesses = []
        if not value:
            return
        elif isinstance(value, list):
            for v in value:
                self.add_weakness(v)
        else:
            self.add_weakness(value)
            
    def add_weakness(self, v):
        """Adds a weakness to the ``weaknesses`` list property.

        Note:
            If ``None`` is passed in no value is added 

        Args:
            v: A weakness value.
            
        Raises: ValueError if the ``v`` param is of type :class:`stix.exploit_target.weakness`
    

        """
        if not v:
            return
        elif isinstance(v, Weakness):
            self.weaknesses.append(v)
        else:
            raise ValueError('Cannot add type %s to weakness list' % type(v))
 
    @property
    def configuration(self):
        """A list of ``Configuration`` objects 
        
        Default Value: ``None``

        Returns:
            A list of
            :class:`stix.exploit_target.configuration`

        Raises:
            ValueError: If set to a value that is not ``None`` and not an
                instance of
                :class:`stix.exploit_target.configuration`

        """
        
        return self._configuration
    
    @configuration.setter
    def configuration(self, value):
        self._configuration = []
        if not value:
            return
        elif isinstance(value, list):
            for v in value:
                self.add_configuration(v)
        else:
            self.add_configuration(value)
            
    def add_configuration(self, v):
        """Adds a configuration to the ``configurations`` list property.

        Note:
            If ``None`` is passed in no value is added 

        Args:
            v: A configuration value.
            
        Raises: ValueError if the ``v`` param is of type :class:`stix.exploit_target.configuration`

        """
        if not v:
            return
        elif isinstance(v, Configuration):
            self.configuration.append(v)
        else:
            raise ValueError('Cannot add type %s to configuration list' % type(v))
 
    def to_obj(self, return_obj=None, ns_info=None):
        super(ExploitTarget, self).to_obj(return_obj=return_obj, ns_info=ns_info)

        if not return_obj:
            return_obj = self._binding_class()

        return_obj.id = self.id_
        return_obj.idref = self.idref
        return_obj.timestamp = dates.serialize_value(self.timestamp)
        return_obj.version = self.version
        return_obj.Title = self.title

        if self.description:
            return_obj.Description = self.description.to_obj(ns_info=ns_info)
        if self.short_description:
            return_obj.Short_Description = self.short_description.to_obj(ns_info=ns_info)
        if self.information_source:
            return_obj.Information_Source = self.information_source.to_obj(ns_info=ns_info)
        if self.handling:
            return_obj.Handling = self.handling.to_obj(ns_info=ns_info)
        if self.potential_coas:
            return_obj.Potential_COAs = self.potential_coas.to_obj(ns_info=ns_info)
        if self.related_exploit_targets:
            return_obj.Related_Exploit_Targets = self.related_exploit_targets.to_obj(ns_info=ns_info)
        if self.vulnerabilities:
            return_obj.Vulnerability = [x.to_obj(ns_info=ns_info) for x in self.vulnerabilities]
        if self.weaknesses:
            return_obj.Weakness = [x.to_obj(ns_info=ns_info) for x in self.weaknesses]
        if self.configuration:
            return_obj.Configuration = [x.to_obj(ns_info=ns_info) for x in self.configuration]
        if self.related_packages:
            return_obj.Related_Packages = self.related_packages.to_obj(ns_info=ns_info)
            
        return return_obj

    @classmethod
    def from_obj(cls, obj, return_obj=None):
        if not obj:
            return None
        if not return_obj:
            return_obj = cls()

        return_obj.id_ = obj.id
        return_obj.idref = obj.idref
        return_obj.timestamp = obj.timestamp # not yet implemented

        if isinstance(obj, cls._binding_class): # TTPType properties
            return_obj.version = obj.version
            return_obj.title = obj.Title
            return_obj.description = StructuredText.from_obj(obj.Description)
            return_obj.short_description = StructuredText.from_obj(obj.Short_Description)
            return_obj.information_source = InformationSource.from_obj(obj.Information_Source)
            return_obj.handling = Marking.from_obj(obj.Handling)
            return_obj.potential_coas = PotentialCOAs.from_obj(obj.Potential_COAs)
            return_obj.related_exploit_targets = RelatedExploitTargets.from_obj(obj.Related_Exploit_Targets)
            return_obj.vulnerabilities = [Vulnerability.from_obj(x) for x in obj.Vulnerability]
            return_obj.weaknesses = [Weakness.from_obj(x) for x in obj.Weakness]
            return_obj.configuration = [Configuration.from_obj(x) for x in obj.Configuration]
            return_obj.related_packages = RelatedPackageRefs.from_obj(obj.Related_Packages)

        return return_obj

    def to_dict(self):
        d = {}
        if self.id_:
            d['id'] = self.id_
        if self.idref:
            d['idref'] = self.idref
        if self.timestamp:
            d['timestamp'] = dates.serialize_value(self.timestamp)
        if self.version:
            d['version'] = self.version
        if self.title:
            d['title'] = self.title
        if self.description:
            d['description'] = self.description.to_dict()
        if self.short_description:
            d['short_description'] = self.short_description.to_dict()
        if self.information_source:
            d['information_source'] = self.information_source.to_dict()
        if self.handling:
            d['handling'] = self.handling.to_dict()
        if self.potential_coas:
            d['potential_coas'] = self.potential_coas.to_dict()
        if self.related_exploit_targets:
            d['related_exploit_targets'] = self.related_exploit_targets.to_dict()
        if self.vulnerabilities:
            d['vulnerabilities'] = [x.to_dict() for x in self.vulnerabilities]
        if self.weaknesses:
            d['weaknesses'] = [x.to_dict() for x in self.weaknesses]
        if self.configuration:
            d['configuration'] = [x.to_dict() for x in self.configuration]
        if self.related_packages:
            d['related_packages'] = self.related_packages.to_dict()

        return d

    @classmethod
    def from_dict(cls, dict_repr, return_obj=None):
        if not dict_repr:
            return None
        if not return_obj:
            return_obj = cls()

        return_obj.id_ = dict_repr.get('id')
        return_obj.idref = dict_repr.get('idref')
        return_obj.timestamp = dict_repr.get('timestamp')
        return_obj.version = dict_repr.get('version')
        return_obj.title = dict_repr.get('title')
        return_obj.description = StructuredText.from_dict(dict_repr.get('description'))
        return_obj.short_description = StructuredText.from_dict(dict_repr.get('short_description'))
        return_obj.information_source = InformationSource.from_dict(dict_repr.get('information_source'))
        return_obj.handling = Marking.from_dict(dict_repr.get('handling'))
        return_obj.potential_coas = PotentialCOAs.from_dict(dict_repr.get('potential_coas'))
        return_obj.related_exploit_targets = RelatedExploitTargets.from_dict(dict_repr.get('related_exploit_targets'))
        return_obj.vulnerabilities = [Vulnerability.from_dict(x) for x in dict_repr.get('vulnerabilities', [])]
        return_obj.weaknesses = [Weakness.from_dict(x) for x in dict_repr.get('weaknesses', [])]
        return_obj.configuration = [Configuration.from_dict(x) for x in dict_repr.get('configuration', [])]
        return_obj.related_packages = RelatedPackageRefs.from_dict(dict_repr.get('related_packages'))
        
        return return_obj

class PotentialCOAs(GenericRelationshipList):
    """
    A list of ``Potential_COA`` objects, defaults to empty array
    """
    _namespace = "http://stix.mitre.org/ExploitTarget-1"
    _binding = exploit_target_binding
    _binding_class = exploit_target_binding.PotentialCOAsType
    _binding_var = "Potential_COA"
    _contained_type = RelatedCOA
    _inner_name = "coas"

    def __init__(self, coas=None, scope=None):
        if coas is None:
            coas = []
        super(PotentialCOAs, self).__init__(*coas, scope=scope)

class RelatedExploitTargets(GenericRelationshipList):
    """
    A list of ``RelatedExploitTargets`` objects, defaults to empty array
    """
    _namespace = "http://stix.mitre.org/ExploitTarget-1"
    _binding = exploit_target_binding
    _binding_class = exploit_target_binding.RelatedExploitTargetsType
    _binding_var = "Related_Exploit_Target"
    _contained_type = RelatedExploitTarget
    _inner_name = "related_exploit_targets"

    def __init__(self, related_exploit_targets=None, scope=None):
        if related_exploit_targets is None:
            related_exploit_targets = []
        super(RelatedExploitTargets, self).__init__(*related_exploit_targets, scope=scope)
