# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
All of the MBIM response message type definitions are in this file. These
definitions inherit from MBIMControlMessage.

Reference:
    [1] Universal Serial Bus Communications Class Subclass Specification for
        Mobile Broadband Interface Model
        http://www.usb.org/developers/docs/devclass_docs/
        MBIM10Errata1_073013.zip
"""
import logging

from autotest_lib.client.cros.cellular.mbim_compliance import mbim_constants
from autotest_lib.client.cros.cellular.mbim_compliance import mbim_errors
from autotest_lib.client.cros.cellular.mbim_compliance import mbim_message


class MBIMControlMessageResponse(mbim_message.MBIMControlMessage):
    """ MBIMMessage Response Message base class. """

    MESSAGE_TYPE = mbim_message.MESSAGE_TYPE_RESPONSE
    _FIELDS = (('I', 'message_type', mbim_message.FIELD_TYPE_PAYLOAD_ID),
               ('I', 'message_length', mbim_message.FIELD_TYPE_TOTAL_LEN),
               ('I', 'transaction_id', ''))


class MBIMOpenDone(MBIMControlMessageResponse):
    """ The class for MBIM_OPEN_DONE. """

    _FIELDS = (('I', 'status_codes', ''),)
    _IDENTIFIERS = {'message_type': mbim_constants.MBIM_OPEN_DONE}


class MBIMCloseDone(MBIMControlMessageResponse):
    """ The class for MBIM_CLOSE_DONE. """

    _FIELDS = (('I', 'status_codes', ''),)
    _IDENTIFIERS = {'message_type': mbim_constants.MBIM_CLOSE_DONE}


class MBIMCommandDoneSecondary(MBIMControlMessageResponse):
    """ The class for MBIM_COMMAND_DONE. """

    _FIELDS = (('I', 'total_fragments', mbim_message.FIELD_TYPE_NUM_FRAGMENTS),
               ('I', 'current_fragment', ''))


class MBIMCommandDone(MBIMControlMessageResponse):
    """ The class for MBIM_COMMAND_DONE. """

    _FIELDS = (('I', 'total_fragments', mbim_message.FIELD_TYPE_NUM_FRAGMENTS),
               ('I', 'current_fragment', ''),
               ('16s', 'device_service_id', mbim_message.FIELD_TYPE_PAYLOAD_ID),
               ('I', 'cid', mbim_message.FIELD_TYPE_PAYLOAD_ID),
               ('I', 'status_codes', ''),
               ('I', 'information_buffer_length',
                mbim_message.FIELD_TYPE_PAYLOAD_LEN))
    _IDENTIFIERS = {'message_type': mbim_constants.MBIM_COMMAND_DONE}
    _SECONDARY_FRAGMENT = MBIMCommandDoneSecondary


class MBIMIndicateStatusSecondary(MBIMControlMessageResponse):
    """ The class for MBIM_INDICATE_STATUS_MSG. """

    _FIELDS = (('I', 'total_fragments', mbim_message.FIELD_TYPE_NUM_FRAGMENTS),
               ('I', 'current_fragment', ''))


class MBIMIndicateStatus(MBIMControlMessageResponse):
    """ The class for MBIM_INDICATE_STATUS_MSG. """

    _FIELDS = (('I', 'total_fragments', mbim_message.FIELD_TYPE_NUM_FRAGMENTS),
               ('I', 'current_fragment', ''),
               ('16s', 'device_service_id', mbim_message.FIELD_TYPE_PAYLOAD_ID),
               ('I', 'cid', mbim_message.FIELD_TYPE_PAYLOAD_ID),
               ('I', 'information_buffer_length',
                mbim_message.FIELD_TYPE_PAYLOAD_LEN))
    _IDENTIFIERS = {'message_type': mbim_constants.MBIM_INDICATE_STATUS_MSG}
    _SECONDARY_FRAGMENT = MBIMIndicateStatusSecondary


class MBIMFunctionError(MBIMControlMessageResponse):
    """ The class for MBIM_FUNCTION_ERROR_MSG. """

    _FIELDS = (('I', 'error_status_code', ''),)
    _IDENTIFIERS = {'message_type': mbim_constants.MBIM_FUNCTION_ERROR_MSG}


def reassemble_response_packets(primary_fragment, secondary_packets):
    """
    Reassembles fragmented response messages into a single object.

    It parses all the secondary fragments as |secondary_frag_class| and
    merges all the payload_buffer fields into the primary fragment.

    @param primary_fragment: Primary fragment message object.
    @param secondary_packets: Array of the raw byte array response received
                               from device.
    @returns Reassembled Response Message object.

    """
    secondary_frag_class = primary_fragment.get_secondary_fragment()
    # Check if we can reassemble at this tree level or not. If there is
    # no associated _SECONDARY_FRAG_CLASS, we need to go down the tree further
    # to reassemble.
    if not secondary_frag_class:
        return None

    for packet in secondary_packets:
        secondary_fragment = secondary_frag_class(raw_data=packet)
        primary_fragment.payload_buffer.extend(
            secondary_fragment.payload_buffer)

    payload_len = primary_fragment.get_payload_len()
    num_fragments = primary_fragment.get_num_fragments()
    if ((num_fragments != len(secondary_packets) + 1) or
        (payload_len != len(primary_fragment.payload_buffer))):
        mbim_errors.log_and_raise(mbim_errors.MBIMComplianceAssertionError,
                                  'mbim1.0:9.2')
    total_length = primary_fragment.calculate_total_len()
    primary_fragment =  primary_fragment.copy(message_length=total_length)
    logging.debug('Reassembled response-> Fragments: %d, Payload length: %d',
                  num_fragments, payload_len)
    return primary_fragment


def parse_response_packets(packets):
    """
    Parses the incoming raw data |packets| into corresponding message response
    object.

    The function starts the at the root of the message hierarchy tree
    and then goes down the root to find the exact leaf node message class. If
    there are multiple frgaments expected at any level, it will reassemble the
    secondary fragments before proceeding.

    @param packets: Array of the raw byte array response received from device.
    @returns Response Message object.

    """
    # Start with the root class for all responses and then go down the tree.
    message_class = MBIMControlMessageResponse
    parse_packets = packets

    while message_class is not None:
        first_packet = parse_packets[0]
        message = message_class(raw_data=first_packet)
        # If there are secondary fragments expected at this level,
        # let's reassemble the payload together before traversing down the
        # message heirarchy.
        if len(parse_packets) > 1:
            reassembled_message = reassemble_response_packets(message,
                                                              parse_packets[1:])
            if reassembled_message is not None:
                message = reassembled_message
                reassembled_packet = message.create_raw_data()
                parse_packets = [reassembled_packet]
        message_class = message.find_payload_class()
    logging.debug("Response Message parsed: %s", message)
    return message