# -*- coding: utf-8 -*-

"""
Encapsulated Interface (MEI) Transport Messages
-----------------------------------------------

"""
import struct
from pymodbus3.constants import DeviceInformation, MoreData
from pymodbus3.pdu import ModbusRequest
from pymodbus3.pdu import ModbusResponse
from pymodbus3.device import ModbusControlBlock
from pymodbus3.device import DeviceInformationFactory
from pymodbus3.pdu import ModbusExceptions

_MCB = ModbusControlBlock()


class ReadDeviceInformationRequest(ModbusRequest):
    """
    This function code allows reading the identification and additional
    information relative to the physical and functional description of a
    remote device, only.

    The Read Device Identification interface is modeled as an address space
    composed of a set of addressable data elements. The data elements are
    called objects and an object Id identifies them.
    """
    function_code = 0x2b
    sub_function_code = 0x0e
    _rtu_frame_size = 3

    def __init__(self, read_code=None, object_id=0x00, **kwargs):
        """ Initializes a new instance

        :param read_code: The device information read code
        :param object_id: The object to read from
        """
        ModbusRequest.__init__(self, **kwargs)
        self.read_code = read_code or DeviceInformation.Basic
        self.object_id = object_id

    def encode(self):
        """ Encodes the request packet

        :returns: The byte encoded packet
        """
        packet = struct.pack(
            '>BBB',
            self.sub_function_code,
            self.read_code,
            self.object_id
        )
        return packet

    def decode(self, data):
        """ Decodes data part of the message.

        :param data: The incoming data
        """
        params = struct.unpack('>BBB', data)
        self.sub_function_code, self.read_code, self.object_id = params

    def execute(self, context):
        """ Run a read exception status request against the store

        :param context: The datastore to request from
        :returns: The populated response
        """
        if not (0x00 <= self.object_id <= 0xff):
            return self.do_exception(ModbusExceptions.IllegalValue)
        if not (0x00 <= self.read_code <= 0x04):
            return self.do_exception(ModbusExceptions.IllegalValue)

        information = DeviceInformationFactory.get(
            _MCB, self.read_code, self.object_id
        )
        return ReadDeviceInformationResponse(self.read_code, information)

    def __str__(self):
        """ Builds a representation of the request

        :returns: The string representation of the request
        """
        return 'ReadDeviceInformationRequest({0},{1})'.format(
            self.read_code, self.object_id
        )


class ReadDeviceInformationResponse(ModbusResponse):
    """
    """
    function_code = 0x2b
    sub_function_code = 0x0e

    @classmethod
    def calculate_rtu_frame_size(cls, data):
        """ Calculates the size of the message

        :param data: A buffer containing the data that have been received.
        :returns: The number of bytes in the response.
        """
        size = 8  # skip the header information
        if len(data) <= 7:
            return size + 2
        count = data[7]

        while count > 0:
            if len(data) < size + 2:
                return size + 2
            _, object_length = struct.unpack('>BB', data[size:size+2])
            size += object_length + 2
            count -= 1
        return size + 2

    def __init__(self, read_code=None, information=None, **kwargs):
        """ Initializes a new instance

        :param read_code: The device information read code
        :param information: The requested information request
        """
        ModbusResponse.__init__(self, **kwargs)
        self.read_code = read_code or DeviceInformation.Basic
        self.information = information or {}
        self.number_of_objects = len(self.information)
        self.conformity = 0x83  # I support everything right now

        # TODO calculate
        self.next_object_id = 0x00  # self.information[-1](0)
        self.more_follows = MoreData.Nothing

    def encode(self):
        """ Encodes the response

        :returns: The byte encoded message
        """
        packet = struct.pack(
            '>BBBBBB',
            self.sub_function_code,
            self.read_code,
            self.conformity,
            self.more_follows,
            self.next_object_id,
            self.number_of_objects
        )

        for (object_id, data) in self.information.items():
            packet += struct.pack('>BB', object_id, len(data))
            packet += data

        return packet

    def decode(self, data):
        """ Decodes a the response

        :param data: The packet data to decode
        """
        params = struct.unpack('>BBBBBB', data[0:6])
        self.sub_function_code, self.read_code = params[0:2]
        self.conformity, self.more_follows = params[2:4]
        self.next_object_id, self.number_of_objects = params[4:6]
        self.information, count = {}, 6  # skip the header information

        while count < len(data):
            object_id, object_length = struct.unpack(
                '>BB', data[count:count+2]
            )
            count += object_length + 2
            self.information[object_id] = data[count-object_length:count]

    def __str__(self):
        """ Builds a representation of the response

        :returns: The string representation of the response
        """
        return 'ReadDeviceInformationResponse({0})'.format(self.read_code)


# Exported symbols
__all__ = [
    'ReadDeviceInformationRequest', 'ReadDeviceInformationResponse',
]
