class Element(ElementBase):
    """
    Base element with common methods shared by inheriting classes.
    If stashing attributes on this class, be sure to prefix with
    an underscore to avoid having the attributes serialized when
    calling update.
    """
    href = ElementLocator()  # : href of this resource

    def __init__(self, name, **meta):
        if meta:
            meta.update(name=name)
        super(Element, self).__init__(**meta)
        self._name = name  # <str>
    
    def __eq__(self, other):
        if isinstance(other, Element):
            return self.name == other.name and self.typeof == other.typeof
        return False
    
    def __ne__(self, other):
        return not self == other
    
    def __hash__(self):
        return hash((self.name, self.typeof))

    @classproperty
    def objects(self):
        """
        Return a Collection Manager of element type

        :return: CollectionManager of the current element type
        :rtype CollectionManager
        """
        return smc.base.collection.CollectionManager(self)

    @classmethod
    def get(cls, name, raise_exc=True):
        """
        Get the element by name. Does an exact match by element type.
        
        :param str name: name of element
        :param bool raise_exc: optionally disable exception. 
        :raises ElementNotFound: if element does not exist
        :rtype: Element
        """
        element = cls.objects.filter(name, exact_match=True).first() if \
            name is not None else None
        if not element and raise_exc:
            raise ElementNotFound('Cannot find specified element: %s, type: '
                '%s' % (name, cls.__name__))
        return element 
        
    @classmethod
    def get_or_create(cls, filter_key=None, with_status=False, **kwargs):
        """
        Convenience method to retrieve an Element or create if it does not
        exist. If an element does not have a `create` classmethod, then it
        is considered read-only and the request will be redirected to :meth:`~get`.
        Any keyword arguments passed except the optional filter_key
        will be used in a create() call. If filter_key is provided, this
        should define an attribute and value to use for an exact match on
        the element. Valid attributes are ones required on the elements
        ``create`` method or can be viewed by the elements class docs.
        If no filter_key is provided, the name field will be used to
        find the element.
        ::

            >>> Network.get_or_create(
                    filter_key={'ipv4_network': '123.123.123.0/24'},
                    name='mynetwork',
                    ipv4_network='123.123.123.0/24')
            Network(name=mynetwork)

        The kwargs should be used to satisfy the elements ``create``
        classmethod parameters to create in the event it cannot be found.

        :param dict filter_key: filter key represents the data attribute and
            value to use to find the element. If none is provided, the name
            field will be used.
        :param kwargs: keyword arguments mapping to the elements ``create``
            method.
        :param bool with_status: if set to True, a tuple is returned with
            (Element, created), where the second tuple item indicates if
            the element has been created or not.
        :raises CreateElementFailed: could not create element with reason
        :raises ElementNotFound: if read-only element does not exist
        :return: element instance by type
        :rtype: Element
        """
        was_created = False
        if 'name' not in kwargs:
            raise ElementNotFound('Name field is a required parameter '
                'for all create or update_or_create type operations on an element')

        if filter_key:
            elements = cls.objects.filter(**filter_key)
            element = elements.first() if elements.exists() else None
        else:
            try:
                element = cls.get(kwargs.get('name'))
            except ElementNotFound:
                if not hasattr(cls, 'create'):
                    raise CreateElementFailed('%s: %r not found and this element '
                        'type does not have a create method.' %
                        (cls.__name__, kwargs['name']))
                element = None
        
        if not element:
            params = {k: v() if callable(v) else v
                      for k, v in kwargs.items()}
            try:
                element = cls.create(**params)
                was_created = True
            except TypeError:
                raise CreateElementFailed('%s: %r not found and missing '
                    'constructor arguments to properly create.' %
                    (cls.__name__, kwargs['name']))

        if with_status:
            return element, was_created
        return element
    
    @classmethod
    def update_or_create(cls, filter_key=None, with_status=False, **kwargs):
        """
        Update or create the element. If the element exists, update it using the
        kwargs provided if the provided kwargs after resolving differences from
        existing values. When comparing values, strings and ints are compared
        directly. If a list is provided and is a list of strings, it will be 
        compared and updated if different. If the list contains unhashable elements,
        it is skipped. To handle complex comparisons, override this method on
        the subclass and process the comparison seperately.
        If an element does not have a `create` classmethod, then it
        is considered read-only and the request will be redirected to
        :meth:`~get`. Provide a ``filter_key`` dict key/value if you want to
        match the element by a specific attribute and value. If no
        filter_key is provided, the name field will be used to find the
        element.
        ::

            >>> host = Host('kali')
            >>> print(host.address)
            12.12.12.12
            >>> host = Host.update_or_create(name='kali', address='10.10.10.10')
            >>> print(host, host.address)
            Host(name=kali) 10.10.10.10

        :param dict filter_key: filter key represents the data attribute and
            value to use to find the element. If none is provided, the name
            field will be used.
        :param kwargs: keyword arguments mapping to the elements ``create``
            method.
        :param bool with_status: if set to True, a 3-tuple is returned with
            (Element, modified, created), where the second and third tuple
            items are booleans indicating the status
        :raises CreateElementFailed: could not create element with reason
        :raises ElementNotFound: if read-only element does not exist
        :return: element instance by type
        :rtype: Element
        """
        updated = False
        # Providing this flag will return before updating and require the calling
        # class to call update if changes were made.
        defer_update = kwargs.pop('defer_update', False)
        if defer_update:
            with_status = True
        
        element, created = cls.get_or_create(filter_key=filter_key, with_status=True, **kwargs)
        if not created:
            for key, value in kwargs.items():
                # Callable, Element or string
                if callable(value):
                    value = value()
                elif isinstance(value, Element):
                    value = value.href
                # Retrieve the 'type' of instance attribute. This is used to
                # serialize attributes that resolve href's to elements. It
                # provides a common structure but also prevents the fetching
                # of the href to element when doing an equality comparison
                attr_type = getattr(type(element), key, None)
                if isinstance(attr_type, ElementRef):
                    attr_name = getattr(attr_type, 'attr', None)
                    if element.data.get(attr_name) != value:
                        element.data[attr_name] = value
                        updated = True
                    continue
                elif isinstance(attr_type, ElementList):
                    value_hrefs = element_resolver(value) # Resolve the elements to href
                    attr_name = getattr(attr_type, 'attr', None)
                    if set(element.data.get(attr_name, [])) != set(value_hrefs):
                        element.data[attr_name] = value_hrefs
                        updated = True
                    continue
                
                # Type is not 'special', therefore we are expecting only strings,
                # integer types or list of strings. Complex data structures
                # will be handled later through encapsulation and __eq__, __hash__
                # for comparison. The keys value type here is going to assume the
                # provided value is of the right type as the key may not necessarily
                # exist in the cached json.
                if isinstance(value, (string_types, int)): # also covers bool
                    val = getattr(element, key, None)
                    if val != value:
                        element.data[key] = value
                        updated = True
                elif isinstance(value, list) and all(isinstance(s, string_types) for s in value):
                    # List of simple strings (assuming the attribute is also!)
                    if set(value) ^ set(element.data.get(key, [])):
                        element.data[key] = value
                        updated = True
                # Complex lists, objects, etc will fall down here and be skipped.
                # To process these, provide defer_update=True, override update_or_create,
                # process the complex object deltas and call update()
            
            if updated and not defer_update:
                element.update()
        
        if with_status:
            return element, updated, created
        return element

    @property
    def name(self):
        """
        Name of element
        """
        return bytes_to_unicode(self._name)

    @property
    def comment(self):
        """
        Comment for element
        """
        return self.data.get('comment', None)

    @comment.setter
    def comment(self, comment):
        self.data['comment'] = comment

    def rename(self, name):
        """
        Rename this element.

        :param str name: new name of element
        :raises UpdateElementFailed: update failed with reason
        :return: None
        """
        self.update(name=name)
    
    def add_category(self, category):
        """
        Category Tags are used to characterize an element by a type
        identifier. They can then be searched and returned as a group
        of elements. If the category tag specified does not exist, it
        will be created. This change will take effect immediately.

        :param list tags: list of category tag names to add to this
            element
        :type tags: list(str)
        :raises ElementNotFound: Category tag element name not found
        :return: None

        .. seealso:: :class:`smc.elements.other.Category`
        """
        assert isinstance(category, list), 'Category input was expecting list.'
        from smc.elements.other import Category
        for tag in category:
            category = Category(tag)
            try:
                category.add_element(self.href)
            except ElementNotFound:
                Category.create(name=tag)
                category.add_element(self.href)

    @property
    def categories(self):
        """
        Search categories assigned to this element
        ::

            >>> from smc.elements.network import Host
            >>> Host('kali').categories
            [Category(name=foo), Category(name=foocategory)]

        :rtype: list(Category)
        """
        return [Element.from_meta(**tag)
                for tag in self.make_request(
                    resource='search_category_tags_from_element')]

    def export(self, filename='element.zip'):
        """
        Export this element.

        Usage::

            engine = Engine('myfirewall')
            extask = engine.export(filename='fooexport.zip')
            while not extask.done():
                extask.wait(3)
            print("Finished download task: %s" % extask.message())
            print("File downloaded to: %s" % extask.filename)

        :param str filename: filename to store exported element
        :raises TaskRunFailed: invalid permissions, invalid directory, or this
            element is a system element and cannot be exported.
        :return: DownloadTask
        
        .. note:: It is not possible to export system elements
        """
        from smc.administration.tasks import Task
        return Task.download(self, 'export', filename)

    @property
    def referenced_by(self):
        """
        Show all references for this element. A reference means that this
        element is being used, for example, in a policy rule, as a member of
        a group, etc.

        :return: list referenced elements
        :rtype: list(Element)
        """
        href = fetch_entry_point('references_by_element')
        return [Element.from_meta(**ref)
                for ref in self.make_request(
                    method='create',
                    href=href,
                    json={'value': self.href})]

    @property
    def history(self):
        """
        .. versionadded:: 0.5.7
            Requires SMC version >= 6.3.2
        
        Obtain the history of this element. This will not chronicle every
        modification made over time, but instead a current snapshot with
        historical information such as when the element was created, by
        whom, when it was last modified and it's current state.
        
        :raises ResourceNotFound: If not running SMC version >= 6.3.2
        :rtype: History
        """
        from smc.core.resource import History
        return History(**self.make_request(resource='history'))
        
    def duplicate(self, name):
        """
        .. versionadded:: 0.5.8
            Requires SMC version >= 6.3.2
        
        Duplicate this element. This is a shortcut method that will make
        a direct copy of the element under the new name and type.
        
        :param str name: name for the duplicated element
        :raises ActionCommandFailed: failed to duplicate the element
        :return: the newly created element
        :rtype: Element
        """
        dup = self.make_request(
            method='update',
            raw_result=True,
            resource='duplicate',
            params={'name': name})
        return type(self)(name=name, href=dup.href, type=type(self).typeof) class SubElement(ElementBase):
    """
    SubElement is the base class for elements that do not
    have direct entry points in the SMC and instead are obtained
    through a reference. They are not 'loaded' directly as are
    classes that inherit from :class:`Element`.
    """

    def __init__(self, **meta):
        super(SubElement, self).__init__(**meta)

    @property
    def name(self):
        return self._meta.name if self._meta else None

    @property
    def href(self):
        return self._meta.href if self._meta else None @create_hook
def ElementCreator(cls, json, **kwargs):
    """
    Helper method for creating elements. If the created element type is
    a SubElement class type, provide href value as kwargs since that class
    type does not have a direct entry point. This is a lazy load that will
    provide only the meta for the element. Additional attribute access
    will load the full data.

    :param Element,SubElement cls: class for creating
    :param dict json: json payload
    :param SMCException exception: exception class to override
    :return: instance of type Element with meta
    :rtype: Element
    """
    if 'exception' not in kwargs:
        kwargs.update(exception=CreateElementFailed)
    href = kwargs.pop('href') if 'href' in kwargs else cls.href
    
    result = SMCRequest(
        href=href,
        json=json,
        **kwargs).create()
    
    element = cls(
        name=json.get('name'),
        type=cls.typeof,
        href=result.href)
    
    if result.user_session.in_atomic_block:
        result.user_session.transactions.append(element)
    return element class LogicalInterface(Element):
    """
    Logical interface is used on either inline or capture interfaces. If an
    engine has both inline and capture interfaces (L2 Firewall or IPS role),
    then you must use a unique Logical Interface on the interface type.

    Create a logical interface::

        LogicalInterface.create('mylogical_interface')  
    """
    typeof = 'logical_interface'

    @classmethod
    def create(cls, name, comment=None):
        """    
        Create the logical interface

        :param str name: name of logical interface
        :param str comment: optional comment
        :raises CreateElementFailed: failed creating element with reason
        :return: instance with meta
        :rtype: LogicalInterface
        """
        json = {'name': name,
                'comment': comment}

        return ElementCreator(cls, json) class ElementNotFound(SMCException):
    """
    Generic exception when an attempt is made to load an element
    that is not found.
    """ class MissingRequiredInput(SMCException):
    """
    Some functinos will flat out fail if certain fields are not provided.
    This is to ensure that some functions have some protection in case the
    user doesn't read the doc's.
    """ class CreateRuleFailed(SMCException):
    """
    Indicates a failed response when creating a rule of any type.
    """ class PolicyCommandFailed(SMCException):
    """
    Generic policy related command failures such as opening
    or closing a VPN policy.
    """ class Action(NestedDict): 
    """ 
    This represents the action associated with the rule. 
    """ 
    def __init__(self, rule=None):
        if rule is None:
            action = dict(action='allow')
            conn_tracking = ConnectionTracking()
            action.update(connection_tracking_options=conn_tracking.data)
            action.update(scan_detection='undefined') 
        else: 
            action = rule.data.get('action', {})
        super(Action, self).__init__(data=action)
 
    @property 
    def action(self): 
        """ 
        Action set for this rule 
 
        :param str value: allow\|discard\|continue\|refuse\|jump\|apply_vpn 
                          \|enforce_vpn\|forward_vpn\|blacklist
        :rtype: str 
        """ 
        return self.get('action') 
 
    @action.setter 
    def action(self, value): 
        self.update(action=value) 
 
    @property
    def connection_tracking_options(self): 
        """ 
        Enables connection tracking. 
        The firewall allows or discards packets according to the selected Connection 
        Tracking mode. Reply packets are allowed as part of the allowed connection 
        without an explicit Access rule. Protocols that use a dynamic port assignment 
        must be allowed using a Service with the appropriate Protocol Agent for that 
        protocol (in Access rules and NAT rules). 
 
        :rtype: ConnectionTracking
        """
        return ConnectionTracking(self) 
    
    @property
    def decrypting(self):
        """
        .. versionadded:: 0.6.0
            Requires SMC version >= 6.3.3
        
        Whether the decryption is enabled on this rule.
        
        :param bool value: True, False, None (inherit from continue rule) 
        :rtype: bool
        """
        return self.get('decrypting')
    
    @decrypting.setter
    def decrypting(self, value):
        self.update(decrypting=value) 
        
    @property 
    def deep_inspection(self): 
        """ 
        Selects traffic that matches this rule for checking against the Inspection 
        Policy referenced by this policy. Traffic is inspected as the Protocol that 
        is attached to the Service element in this rule. 
 
        :param bool value: True, False, None (inherit from continue rule) 
        :rtype: bool 
        """ 
        return self.get('deep_inspection') 
 
    @deep_inspection.setter 
    def deep_inspection(self, value): 
        self.update(deep_inspection=value) 
 
    @property 
    def file_filtering(self): 
        """ 
        (IPv4 Only) Inspects matching traffic against the File Filtering policy. 
        Selecting this option should also activates the Deep Inspection option. 
        You can further adjust virus scanning in the Inspection Policy.  
 
        :param bool value: True, False, None (inherit from continue rule) 
        :rtype: bool 
        """ 
        return self.get('file_filtering') 
 
    @file_filtering.setter 
    def file_filtering(self, value): 
        self.update(file_filtering=value) 
 
    @property 
    def dos_protection(self): 
        """ 
        Enable or disable DOS protection mode 
 
        :param bool value: True, False, None (inherit from continue rule) 
        :rtype: bool 
        """ 
        return self.get('dos_protection') 
 
    @dos_protection.setter 
    def dos_protection(self, value): 
        self.update(dos_protection=value) 
 
    @property 
    def scan_detection(self): 
        """ 
        Enable or disable Scan Detection for traffic that matches the 
        rule. This overrides the option set in the Engine properties. 
 
        Enable scan detection on this rule:: 
 
            for rule in policy.fw_ipv4_access_rules.all(): 
                rule.action.scan_detection = 'on' 
 
        :param str value: on\|off\|undefined 
        :return: scan detection setting (on,off,undefined) 
        :rtype: str 
        """ 
        return self.get('scan_detection') 
 
    @scan_detection.setter 
    def scan_detection(self, value): 
        if value in ('on', 'off', 'undefined'): 
            self.update(scan_detection=value) 
 
    @property 
    def sub_policy(self): 
        """ 
        Sub policy is used when ``action=jump``. 
         
        :rtype: FirewallSubPolicy 
        """ 
        if 'sub_policy' in self: 
            return Element.from_href(self.get('sub_policy')) 
 
    @sub_policy.setter 
    def sub_policy(self, value): 
        self.update(sub_policy=element_resolver(value))
 
    @property 
    def user_response(self): 
        """ 
        Read-only user response setting 
        """ 
        return self.get('user_response') 
 
    @property 
    def vpn(self): 
        """ 
        Return vpn reference. Only used if 'enforce_vpn', 'apply_vpn', 
        or 'forward_vpn' is the action type. 
     
        :param PolicyVPN value: set the policy VPN for VPN action
        :rtype: PolicyVPN 
        """ 
        if 'vpn' in self: 
            return Element.from_href(self.get('vpn'))
 
    @vpn.setter 
    def vpn(self, value):
        self.update(vpn=element_resolver(value))

    @property 
    def mobile_vpn(self): 
        """ 
        Mobile VPN only applies to engines that support VPN and that 
        have the action of 'enforce_vpn', 'apply_vpn' or 'forward_vpn' 
        set. This will enable mobile VPN traffic on this VPN rule. 
 
        :param boolean value: set mobile vpn on or off
        :rtype: boolean 
        """ 
        return self.get('mobile_vpn') 
 
    @mobile_vpn.setter 
    def mobile_vpn(self, value): 
        self.update(mobile_vpn=value) class LogOptions(NestedDict):
    """
    Log Options represent the settings related to per rule logging.

    Example of obtaining a rule reference and turning logging on 
    for a particular rule::

        policy = FirewallPolicy('smcpython')
        for rule in policy.fw_ipv4_access_rules.all():
            if rule.name == 'foo':
                rule.options.log_accounting_info_mode = True
                rule.options.log_level = 'stored'
                rule.options.application_logging = 'enforced'
                rule.options.user_logging = 'enforced'
                rule.save()
    """
    def __init__(self, rule=None):
        if rule is None:
            logopts = dict(
                log_accounting_info_mode=False,
                log_closing_mode=True,
                log_level='undefined',
                log_payload_additionnal=False,
                log_payload_excerpt=False,
                log_payload_record=False,
                log_severity=-1)
        else:
            logopts = rule.data.get('options', {})
        super(LogOptions, self).__init__(data=logopts)
        
    @property
    def application_logging(self):
        """
        Stores information about Application use. You can log spplication
        use even if you do not use Applications for access control.

        :param str value: off\|default\|enforced
        :return: str
        """
        return self.get('application_logging')

    @application_logging.setter
    def application_logging(self, value):
        if value in ('off', 'default', 'enforced'):
            self.update(application_logging=value)

    @property
    def log_accounting_info_mode(self):
        """
        Both connection opening and closing are logged and information
        on the volume of traffic is collected. This option is not 
        available for rules that issue alerts.
        If you want to create reports that are based on traffic volume, 
        you must select this option for all rules that allow traffic that
        you want to include in the reports.

        :param bool value: log accounting information (bits/bytes transferred)
        :return: bool 
        """
        return self.get('log_accounting_info_mode')

    @log_accounting_info_mode.setter
    def log_accounting_info_mode(self, value):
        self.update(log_accounting_info_mode=value)
        
    @property
    def log_closing_mode(self):
        """
        Specifying False means no log entries are created when 
        connections are closed. True will mean both connection 
        opening and closing are logged, but no information is 
        collected on the volume of traffic.

        :param bool value: enable/disable accounting data
        :return: bool
        """
        return self.get('log_closing_mode')

    @log_closing_mode.setter
    def log_closing_mode(self, value):
        self.update(log_closing_mode=value)

    @property
    def log_level(self):
        """
        Configure per rule logging. It is recommended to configure an
        Any/Any/Any/Continue rule in position 1 if global logging is 
        required. This can be used to override any global logging setting.

        :param str value: none\|stored\|transient\|essential\|alert\|undefined
        :return: str
        """
        return self.get('log_level')

    @log_level.setter
    def log_level(self, value):
        if value in ('none', 'stored', 'transient', 'essential', 'alert'):
            self.update(log_level=value)
        if not self.log_accounting_info_mode:
            self.log_accounting_info_mode = True

    @property
    def log_payload_additional(self):
        """
        Stores packet payload extracted from the traffic. The collected 
        payload provides information for some of the additional log fields
        depending on the type of traffic.

        :param bool value: True, False
        :return: bool
        """
        return self.get('log_payload_additionnal')

    @log_payload_additional.setter
    def log_payload_additional(self, value):
        self.update(log_payload_additionnal=value)

    @property
    def log_payload_excerpt(self):
        """
        Stores an excerpt of the packet that matched. The maximum recorded
        excerpt size is 4 KB. This allows quick viewing of the payload in
        the logs view.

        :param bool value: collect excerpt or not
        :return: bool
        """
        return self.get('log_payload_excerpt')

    @log_payload_excerpt.setter
    def log_payload_excerpt(self, value):
        self.update(log_payload_excerpt=value)

    @property
    def log_payload_record(self):
        """
        Records the traffic up to the limit that is set in the Record 
        Length field.

        :param bool value: True, False
        :return: bool
        """
        return self.get('log_payload_record')

    @log_payload_record.setter
    def log_payload_record(self, value):
        self.update(log_payload_record=value)

    @property
    def log_severity(self):
        """
        Read only log severity level

        :return: str
        """
        return self.get('log_severity')

    @property
    def user_logging(self):
        """
        Stores information about Users when they are used as the Source
        or Destination of an Access rule.
        You must select this option if you want Users to be referenced by
        name in log entries, statistics, reports, and user monitoring.
        Otherwise, only the IP address associated with the User at the time
        the log was created is stored.

        :param str value: off\|default\|enforced
        :return: str
        """
        return self.get('user_logging')

    @user_logging.setter
    def user_logging(self, value):
        if value in ('off', 'default', 'enforced'):
            self.update(user_logging=value) class Destination(RuleElement, NestedDict):
    """
    Destination fields for a rule.
    """
    typeof = 'dst'

    def __init__(self, rule=None):
        dests = dict(none=True) if not rule else \
            rule.data.get('destinations')
        self.rule = rule
        super(Destination, self).__init__(data=dests) class Source(RuleElement, NestedDict):
    """
    Source fields for a rule
    """
    typeof = 'src'
        
    def __init__(self, rule=None):
        sources = dict(none=True) if not rule else \
            rule.data.get('sources')
        self.rule = rule
        super(Source, self).__init__(data=sources) class Service(RuleElement, NestedDict):
    """
    Service fields for a rule
    """
    typeof = 'service'

    def __init__(self, rule=None):
        services = dict(none=True) if not rule else \
            rule.data.get('services')
        self.rule = rule
        super(Service, self).__init__(data=services)     class AuthenticationOptions(NestedDict):
    """
    Authentication options are set on a per rule basis and dictate
    whether a user requires identification to match.
    """

    def __init__(self, rule=None):
        if rule is None:
            auth = dict(
                methods=[],
                require_auth=False,
                users=[])
        else: 
            auth = rule.data.get('authentication_options', {})
        super(AuthenticationOptions, self).__init__(data=auth)
    
    def __eq__(self, other):
        if isinstance(other, AuthenticationOptions):
            if self.require_auth != other.require_auth:
                return False
            for values in ('users', 'methods'):
                if set(self.data.get(values, [])) != \
                    set(other.data.get(values, [])):
                    return False
            return True
        return False
    
    def __ne__(self, other):
        return not self.__eq__(other)
    
    @property
    def methods(self):
        """
        Read only authentication methods enabled

        :return: list value: auth methods enabled
        """
        return [Element.from_href(method) for method in self.get('methods')]

    @property
    def require_auth(self):
        """
        Ready only authentication required

        :return: boolean
        """
        return self.get('require_auth')

    @property
    def timeout(self):
        """
        Timeout between authentications

        :return: int
        """
        return self.get('timeout')

    @property
    def users(self):
        """
        List of users required to authenticate

        :return: list
        """
        return [Element.from_href(user) for user in self.get('users', [])] class TimeRange(object):
    """
    Represents a time range setting for a given rule.
    Time ranges can currently be set up to support rules based
    on starting month and ending month. At that time the rule
    will be disabled automatically.
    """

    def __init__(self, data):
        self.data = data

    @property
    def day_ranges(self):
        """
        Not Yet Implemented
        """
        pass

    @property
    def month_range_start(self):
        """
        Starting month for rule validity. Use this with month_range_end.

        :param str jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec
        """
        return self.data.get('month_range_start')

    @month_range_start.setter
    def month_range_start(self, value):
        self.data['month_range_start'] = value

    @property
    def month_range_end(self):
        """
        Set month end range. Use this with month_range_start.

        :param str jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec
        """
        return self.data.get('month_range_end')

    @month_range_end.setter
    def month_range_end(self, value):
        self.data['month_range_end'] = value def element_resolver(elements, do_raise=True):
    """
    Element resolver takes either a single class instance
    or a list of elements to resolve the href. It does
    not assume a specific interface, instead if it's
    a class, it just needs an 'href' attribute that should
    hold the http url for the resource. If a list is
    provided, a list is returned. If you want to suppress
    raising an exception and just return None or [] instead,
    set do_raise=False.

    :raises ElementNotFound: if this is of type Element,
        ElementLocator will attempt to retrieve meta if it
        doesn't already exist but the element was not found.
    """
    if isinstance(elements, list):
        e = []
        for element in elements:
            try:
                e.append(element.href)
            except AttributeError:
                e.append(element)
            except smc.api.exceptions.ElementNotFound:
                if do_raise:
                    raise
        return e
    try:
        return elements.href
    except AttributeError:
        return elements
    except smc.api.exceptions.ElementNotFound:
        if do_raise:
            raise def cacheable_resource(func):
    @functools.wraps(func)
    def get(self):
        try:
            return self._cache[func]
        except AttributeError:
            self._cache = {}
        except KeyError:
            pass
        ret = self._cache[func] = func(self)
        return ret
    return property(get)
from smc.base.model import Element, SubElement, ElementCreator
from smc.elements.other import LogicalInterface
from smc.api.exceptions import ElementNotFound, MissingRequiredInput,\
    CreateRuleFailed, PolicyCommandFailed
from smc.policy.rule_elements import Action, LogOptions, Destination, Source,\
    Service, AuthenticationOptions, TimeRange
from smc.base.util import element_resolver
from smc.base.decorators import cacheable_resource
    """
    def create_rule_section(self, name, add_pos=None, after=None, before=None):
        """
        Create a rule section in a Firewall Policy. To specify a specific numbering
        position for the rule section, use the `add_pos` field. If no position or
        before/after is specified, the rule section will be placed at the top which
        will encapsulate all rules below.
        Create a rule section for the relavant policy::
        
            policy = FirewallPolicy('mypolicy')
            policy.fw_ipv4_access_rules.create_rule_section(name='attop')
            # For NAT rules
            policy.fw_ipv4_nat_rules.create_rule_section(name='mysection', add_pos=5)
        
        :param str name: create a rule section by name
        :param int add_pos: position to insert the rule, starting with position 1.
            If the position value is greater than the number of rules, the rule is
            inserted at the bottom. If add_pos is not provided, rule is inserted in
            position 1. Mutually exclusive with ``after`` and ``before`` params. 
        :param str after: Rule tag to add this rule after. Mutually exclusive with
            ``add_pos`` and ``before`` params. 
        :param str before: Rule tag to add this rule before. Mutually exclusive with
            ``add_pos`` and ``after`` params. 
        :raises MissingRequiredInput: when options are specified the need additional  
            setting, i.e. use_vpn action requires a vpn policy be specified. 
        :raises CreateRuleFailed: rule creation failure 
        :return: the created ipv4 rule 
        :rtype: IPv4Rule 
        """
        href = self.href
        params = None
        if add_pos is not None:
            href = self.add_at_position(add_pos)
        elif before or after: 
            params = self.add_before_after(before, after) 
        
        return ElementCreator(
            self.__class__,
            exception=CreateRuleFailed,
            href=href,
            params=params,
            json={'comment': name})
    
    def add_at_position(self, pos):
        if pos <= 0:
            pos = 1
        rules = self.make_request(href=self.href)
        if rules:
            if len(rules) >= pos:  # Position somewhere in the list
                for position, entry in enumerate(rules):
                    if position + 1 == pos:
                        return self.__class__(**entry).get_relation('add_before')
            else:  # Put at the end
                last_rule = rules.pop()
                return self.__class__(**last_rule).get_relation('add_after')
        return self.href

    def add_before_after(self, before=None, after=None):
        params = None
        if after is not None:
            params = {'after': after}
        elif before is not None:
            params = {'before': before}
        return params
        
    def update_targets(self, sources, destinations, services):
        source = Source()
        destination = Destination()
        service = Service()
    
        if sources is not None:
            if isinstance(sources, str) and sources.lower() == 'any':
                source.set_any()
            else:
                source.add_many(sources)
        else:
            source.set_none()
    
        if destinations is not None:
            if isinstance(destinations, str) and destinations.lower() == 'any':
                destination.set_any()
            else:
                destination.add_many(destinations)
        else:
            destination.set_none()
    
        if services is not None:
            if isinstance(services, str) and services.lower() == 'any':
                service.set_any()
            else:
                service.add_many(services)
        else:
            service.set_none()
    
        e = {}
        #e.update(source())
        e.update(sources=source.data)
        e.update(destinations=destination.data)
        e.update(services=service.data)
        return e
    
    def update_logical_if(self, logical_interfaces):
        e = {}
        if logical_interfaces is None:
            e.update(logical_interfaces={'any': True})
        else:
            try:
                logicals = []
                for interface in logical_interfaces:
                    logicals.append(LogicalInterface(interface).href)
                e.update(logical_interfaces={
                    'logical_interface': logicals})
            
            except ElementNotFound:
                raise MissingRequiredInput(
                    'Cannot find Logical interface specified '
                    ': {}'.format(logical_interfaces))
        return e
        


class IPv4Rule(RuleCommon, Rule, SubElement):