#!/usr/bin/env python

"""
Rebuilt Commandable
"""

from bacpypes.debugging import bacpypes_debugging, ModuleLogger
from bacpypes.consolelogging import ConfigArgumentParser

from bacpypes.core import run
from bacpypes.task import OneShotTask
from bacpypes.errors import ExecutionError

from bacpypes.primitivedata import BitString, CharacterString, Date, Integer, \
    Double, Enumerated, OctetString, Real, Time, Unsigned
from bacpypes.basetypes import BinaryPV, ChannelValue, DateTime, DoorValue, PriorityValue, \
    PriorityArray
from bacpypes.object import Property, ReadableProperty, WritableProperty, \
    register_object_type, \
    AccessDoorObject, AnalogOutputObject, AnalogValueObject, \
    BinaryOutputObject, BinaryValueObject, BitStringValueObject, CharacterStringValueObject, \
    DateValueObject, DatePatternValueObject, DateTimePatternValueObject, \
    DateTimeValueObject, IntegerValueObject, \
    LargeAnalogValueObject, LightingOutputObject, MultiStateOutputObject, \
    MultiStateValueObject, OctetStringValueObject, PositiveIntegerValueObject, \
    TimeValueObject, TimePatternValueObject, ChannelObject

from mstplib import MSTPSimpleApplication
from bacpypes.service.object import CurrentPropertyListMixIn
from bacpypes.service.device import LocalDeviceObject

# some debugging
_debug = 0
_log = ModuleLogger(globals())


#
#   Commandable
#

@bacpypes_debugging
def Commandable(datatype, presentValue='presentValue', priorityArray='priorityArray', relinquishDefault='relinquishDefault'):
    if _debug: Commandable._debug("Commandable %r ...", datatype)

    class _Commando(object):

        properties = [
            WritableProperty(presentValue, datatype),
            ReadableProperty(priorityArray, PriorityArray),
            ReadableProperty(relinquishDefault, datatype),
            ]

        _pv_choice = None

        def __init__(self, **kwargs):
            super(_Commando, self).__init__(**kwargs)

            # build a default value in case one is needed
            default_value = datatype().value
            if issubclass(datatype, Enumerated):
                default_value = datatype._xlate_table[default_value]
            if _debug: Commandable._debug("    - default_value: %r", default_value)

            # see if a present value was provided
            if (presentValue not in kwargs):
                setattr(self, presentValue, default_value)

            # see if a priority array was provided
            if (priorityArray not in kwargs):
                new_priority_array = PriorityArray()
                for i in range(16):
                    new_priority_array.append(PriorityValue(null=()))
                setattr(self, priorityArray, new_priority_array)

            # see if a present value was provided
            if (relinquishDefault not in kwargs):
                setattr(self, relinquishDefault, default_value)

        def _highest_priority_value(self):
            if _debug: Commandable._debug("_highest_priority_value")

            priority_array = getattr(self, priorityArray)
            for i in range(1, 17):
                priority_value = priority_array[i]
                if priority_value.null is None:
                    if _debug: Commandable._debug("    - found at index: %r", i)

                    value = getattr(priority_value, _Commando._pv_choice)
                    value_source = "###"

                    if issubclass(datatype, Enumerated):
                        value = datatype._xlate_table[value]
                        if _debug: Commandable._debug("    - remapped enumeration: %r", value)

                    break
            else:
                value = getattr(self, relinquishDefault)
                value_source = None

            if _debug: Commandable._debug("    - value, value_source: %r, %r", value, value_source)

            # return what you found
            return value, value_source

        def WriteProperty(self, property, value, arrayIndex=None, priority=None, direct=False):
            if _debug: Commandable._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", property, value, arrayIndex, priority, direct)

            # when writing to the presentValue with a priority
            if (property == presentValue):
                if _debug: Commandable._debug("    - writing to %s, priority %r", presentValue, priority)

                # default (lowest) priority
                if priority is None:
                    priority = 16
                if _debug: Commandable._debug("    - translate to priority array, index %d", priority)

                # translate to updating the priority array
                property = priorityArray
                arrayIndex = priority
                priority = None

            # update the priority array entry
            if (property == priorityArray):
                if (arrayIndex is None):
                    if _debug: Commandable._debug("    - writing entire %s", priorityArray)

                    # pass along the request
                    super(_Commando, self).WriteProperty(
                        property, value,
                        arrayIndex=arrayIndex, priority=priority, direct=direct,
                        )
                else:
                    if _debug: Commandable._debug("    - writing to %s, array index %d", priorityArray, arrayIndex)

                    # check the bounds
                    if arrayIndex == 0:
                        raise ExecutionError(errorClass='property', errorCode='writeAccessDenied')
                    if (arrayIndex < 1) or (arrayIndex > 16):
                        raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex')

                    # update the specific priorty value element
                    priority_value = getattr(self, priorityArray)[arrayIndex]
                    if _debug: Commandable._debug("    - priority_value: %r", priority_value)

                    # the null or the choice has to be set, the other clear
                    if value is ():
                        if _debug: Commandable._debug("    - write a null")
                        priority_value.null = value
                        setattr(priority_value, _Commando._pv_choice, None)
                    else:
                        if _debug: Commandable._debug("    - write a value")

                        if issubclass(datatype, Enumerated):
                            value = datatype._xlate_table[value]
                            if _debug: Commandable._debug("    - remapped enumeration: %r", value)

                        priority_value.null = None
                        setattr(priority_value, _Commando._pv_choice, value)

                # look for the highest priority value
                value, value_source = self._highest_priority_value()

                # compare with the current value
                current_value = getattr(self, presentValue)
                if value == current_value:
                    if _debug: Commandable._debug("    - no present value change")
                    return

                # turn this into a present value change
                property = presentValue
                arrayIndex = priority = None

            # allow the request to pass through
            if _debug: Commandable._debug("    - super: %r %r arrayIndex=%r priority=%r", property, value, arrayIndex, priority)

            super(_Commando, self).WriteProperty(
                property, value,
                arrayIndex=arrayIndex, priority=priority, direct=direct,
                )

    # look up a matching priority value choice
    for element in PriorityValue.choiceElements:
        if issubclass(datatype, element.klass):
            _Commando._pv_choice = element.name
            break
    else:
        _Commando._pv_choice = 'constructedValue'
    if _debug: Commandable._debug("    - _pv_choice: %r", _Commando._pv_choice)

    # return the class
    return _Commando

#
#   MinOnOffTask
#

@bacpypes_debugging
class MinOnOffTask(OneShotTask):

    def __init__(self, binary_obj):
        if _debug: MinOnOffTask._debug("__init__ %s", repr(binary_obj))
        OneShotTask.__init__(self)

        # save a reference to the object
        self.binary_obj = binary_obj

        # listen for changes to the present value
        self.binary_obj._property_monitors['presentValue'].append(self.present_value_change)

    def present_value_change(self, old_value, new_value):
        if _debug: MinOnOffTask._debug("present_value_change %r %r", old_value, new_value)

        # if there's no value change, skip all this
        if old_value == new_value:
            if _debug: MinOnOffTask._debug("    - no state change")
            return

        # get the minimum on/off time
        if new_value == 'inactive':
            task_delay = getattr(self.binary_obj, 'minimumOnTime') or 0
            if _debug: MinOnOffTask._debug("    - minimum on: %r", task_delay)
        elif new_value == 'active':
            task_delay = getattr(self.binary_obj, 'minimumOffTime') or 0
            if _debug: MinOnOffTask._debug("    - minimum off: %r", task_delay)
        else:
            raise ValueError("unrecognized present value for %r: %r" % (self.binary_obj.objectIdentifier, new_value))

        # if there's no delay, don't bother
        if not task_delay:
            if _debug: MinOnOffTask._debug("    - no delay")
            return

        # set the value at priority 6
        self.binary_obj.WriteProperty('presentValue', new_value, priority=6)

        # install this to run, if there is a delay
        self.install_task(delta=task_delay)

    def process_task(self):
        if _debug: MinOnOffTask._debug("process_task(%s)", self.binary_obj.objectName)

        # clear the value at priority 6
        self.binary_obj.WriteProperty('presentValue', (), priority=6)

#
#   MinOnOff
#

@bacpypes_debugging
class MinOnOff(object):

    def __init__(self, **kwargs):
        if _debug: MinOnOff._debug("__init__ ...")
        super(MinOnOff, self).__init__(**kwargs)

        # create the timer task
        self._min_on_off_task = MinOnOffTask(self)

#
#   Commandable Standard Objects
#

class AccessDoorObjectCmd(Commandable(DoorValue), AccessDoorObject):
    pass

class AnalogOutputObjectCmd(Commandable(Real), AnalogOutputObject):
    pass

class AnalogValueObjectCmd(Commandable(Real), AnalogValueObject):
    pass

### class BinaryLightingOutputObjectCmd(Commandable(Real), BinaryLightingOutputObject):
###     pass

class BinaryOutputObjectCmd(Commandable(BinaryPV), MinOnOff, BinaryOutputObject):
    pass

class BinaryValueObjectCmd(Commandable(BinaryPV), MinOnOff, BinaryValueObject):
    pass

class BitStringValueObjectCmd(Commandable(BitString), BitStringValueObject):
    pass

class CharacterStringValueObjectCmd(Commandable(CharacterString), CharacterStringValueObject):
    pass

class DateValueObjectCmd(Commandable(Date), DateValueObject):
    pass

class DatePatternValueObjectCmd(Commandable(Date), DatePatternValueObject):
    pass

class DateTimeValueObjectCmd(Commandable(DateTime), DateTimeValueObject):
    pass

class DateTimePatternValueObjectCmd(Commandable(DateTime), DateTimePatternValueObject):
    pass

class IntegerValueObjectCmd(Commandable(Integer), IntegerValueObject):
    pass

class LargeAnalogValueObjectCmd(Commandable(Double), LargeAnalogValueObject):
    pass

class LightingOutputObjectCmd(Commandable(Real), LightingOutputObject):
    pass

class MultiStateOutputObjectCmd(Commandable(Unsigned), MultiStateOutputObject):
    pass

class MultiStateValueObjectCmd(Commandable(Unsigned), MultiStateValueObject):
    pass

class OctetStringValueObjectCmd(Commandable(OctetString), OctetStringValueObject):
    pass

class PositiveIntegerValueObjectCmd(Commandable(Unsigned), PositiveIntegerValueObject):
    pass

class TimeValueObjectCmd(Commandable(Time), TimeValueObject):
    pass

class TimePatternValueObjectCmd(Commandable(Time), TimePatternValueObject):
    pass

#
#   ChannelValueProperty
#

class ChannelValueProperty(Property):

    def __init__(self):
        if _debug: ChannelValueProperty._debug("__init__")
        Property.__init__(self, 'presentValue', ChannelValue, default=None, optional=False, mutable=True)

    def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False):
        if _debug: ChannelValueProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct)

        ### Clause 12.53.5, page 487
        raise NotImplementedError()

#
#   ChannelObjectCmd
#

class ChannelObjectCmd(ChannelObject):

    properties = [
        ChannelValueProperty(),
        ]

##
##
##
##
##

@register_object_type(vendor_id=999)
class LocalAnalogValueObjectCmd(CurrentPropertyListMixIn, AnalogValueObjectCmd):
    pass

@register_object_type(vendor_id=999)
class LocalBinaryOutputObjectCmd(CurrentPropertyListMixIn, BinaryOutputObjectCmd):
    pass

@register_object_type(vendor_id=999)
class LocalDateValueObjectCmd(CurrentPropertyListMixIn, DateValueObjectCmd):
    pass

#
#   __main__
#

def main():
    parser = ConfigArgumentParser(description=__doc__)
    parser.add_argument(
        '--mstp_dir',
        dest='mstp_dir',
        default='/var/tmp',
        help='mstp directory where the agent looks for (default: /var/tmp )'
    )
    args = parser.parse_args()


    if _debug: _log.debug("initialization")
    if _debug: _log.debug("    - args: %r", args)

    # make a device object
    this_device = LocalDeviceObject(
        objectName=args.ini.objectname,
        objectIdentifier=int(args.ini.objectidentifier),
        maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted),
        segmentationSupported=args.ini.segmentationsupported,
        vendorIdentifier=int(args.ini.vendoridentifier),
        _interface=args.ini.interface,
        _mac_address=int(args.ini.address),
        _max_masters=int(args.ini.max_masters),
        _baudrate=int(args.ini.baudrate),
        _maxinfo=int(args.ini.maxinfo),
        _mstp_dir=str(args.mstp_dir)
        )

    # make a sample application
    this_application = MSTPSimpleApplication(this_device, args.ini.address)

    # make a commandable analog value object, add to the device
    avo1 = LocalAnalogValueObjectCmd(
        objectIdentifier=('analogValue', 1),
        objectName='avo1',
        )
    if _debug: _log.debug("    - avo1: %r", avo1)
    this_application.add_object(avo1)

    # make a commandable binary output object, add to the device
    boo1 = LocalBinaryOutputObjectCmd(
        objectIdentifier=('binaryOutput', 1),
        objectName='boo1',
        presentValue='inactive',
        relinquishDefault='inactive',
        minimumOnTime=5,        # let it warm up
        minimumOffTime=10,      # let it cool off
        )
    if _debug: _log.debug("    - boo1: %r", boo1)
    this_application.add_object(boo1)

    # get the current date
    today = Date().now()

    # make a commandable date value object, add to the device
    dvo1 = LocalDateValueObjectCmd(
        objectIdentifier=('dateValue', 1),
        objectName='dvo1',
        presentValue=today.value,
        )
    if _debug: _log.debug("    - dvo1: %r", dvo1)
    this_application.add_object(dvo1)

    if _debug: _log.debug("running")

    run()

    _log.debug("fini")


if __name__ == "__main__":
    main()
