#
# Copyright (C) 2016 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import logging
import sys

from vts.proto import ComponentSpecificationMessage_pb2 as CompSpecMsg


def PyValue2PbEnum(message, pb_spec, py_value):
    """Converts Python value to VTS VariableSecificationMessage (Enum).

    Args:
        message: VariableSpecificationMessage is the current and result
                 value message.
        pb_spec: VariableSpecificationMessage which captures the
                 specification of a target attribute.
        py_value: Python value provided by a test case.

    Returns:
        Converted VariableSpecificationMessage if found, None otherwise
    """
    if pb_spec.name:
        message.name = pb_spec.name
    message.type = CompSpecMsg.TYPE_ENUM
    # Look for the enum definition and retrieve the scalar type.
    scalar_type = pb_spec.enum_value.scalar_type
    if scalar_type != "":
        # If the scalar type definition is found, set it and return.
        setattr(message.scalar_value, scalar_type, py_value)
        return
    # Use default scalar_type int32_t for enum definition if the definition
    # is not found.
    setattr(message.scalar_value, "int32_t", py_value)


def PyValue2PbScalar(message, pb_spec, py_value):
    """Converts Python value to VTS VariableSecificationMessage (Scalar).

    Args:
        message: VariableSpecificationMessage is the current and result
                 value message.
        pb_spec: VariableSpecificationMessage which captures the
                 specification of a target attribute.
        py_value: Python value provided by a test case.

    Returns:
        Converted VariableSpecificationMessage if found, None otherwise
    """
    if pb_spec.name:
        message.name = pb_spec.name
    message.type = CompSpecMsg.TYPE_SCALAR
    message.scalar_type = pb_spec.scalar_type
    setattr(message.scalar_value, pb_spec.scalar_type, py_value)


def PyString2PbString(message, pb_spec, py_value):
    """Converts Python string to VTS VariableSecificationMessage (String).

    Args:
        message: VariableSpecificationMessage is the current and result
                 value message.
        pb_spec: VariableSpecificationMessage which captures the
                 specification of a target attribute.
        py_value: Python value provided by a test case.

    Returns:
        Converted VariableSpecificationMessage if found, None otherwise
    """
    if pb_spec.name:
        message.name = pb_spec.name
    message.type = CompSpecMsg.TYPE_STRING
    message.string_value.message = py_value
    message.string_value.length = len(py_value)


def PyList2PbVector(message, pb_spec, py_value):
    """Converts Python list value to VTS VariableSecificationMessage (Vector).

    Args:
        message: VariableSpecificationMessage is the current and result
                 value message.
        pb_spec: VariableSpecificationMessage which captures the
                 specification of a target attribute.
        py_value: Python value provided by a test case.

    Returns:
        Converted VariableSpecificationMessage if found, None otherwise
    """
    if pb_spec.name:
        message.name = pb_spec.name
    message.type = CompSpecMsg.TYPE_VECTOR
    if len(py_value) == 0:
        return message

    vector_spec = pb_spec.vector_value[0]
    for curr_value in py_value:
        new_vector_message = message.vector_value.add()
        new_vector_message.CopyFrom(Convert(vector_spec, curr_value))
    message.vector_size = len(py_value)
    return message


def FindSubStructType(pb_spec, sub_struct_name):
    """Finds a specific sub_struct type.

    Args:
        pb_spec: VariableSpecificationMessage which captures the
                 specification of a target attribute.
        sub_struct_name: string, the name of a sub struct to look up.

    Returns:
        VariableSpecificationMessage if found or None otherwise.
    """
    for sub_struct in pb_spec.sub_struct:
        if sub_struct.name == sub_struct_name:
            return sub_struct
    return None


def FindSubUnionType(pb_spec, sub_union_name):
    """Finds a specific sub_union type.

    Args:
        pb_spec: VariableSpecificationMessage which captures the
                 specification of a target attribute.
        sub_union_name: string, the name of a sub union to look up.

    Returns:
        VariableSpecificationMessage if found or None otherwise.
    """
    for sub_union in pb_spec.sub_union:
        if sub_union.name == sub_union_name:
            return sub_union
    return None


def PyDict2PbStruct(message, pb_spec, py_value):
    """Converts Python dict to VTS VariableSecificationMessage (struct).

    Args:
        pb_spec: VariableSpecificationMessage which captures the
                 specification of a target attribute.
        py_value: A dictionary that represents a struct.

    Returns:
        Converted VariableSpecificationMessage if found, None otherwise
    """
    if pb_spec.name:
        message.name = pb_spec.name
    message.type = CompSpecMsg.TYPE_STRUCT
    provided_attrs = set(py_value.keys())
    for attr in pb_spec.struct_value:
        if attr.name in py_value:
            provided_attrs.remove(attr.name)
            curr_value = py_value[attr.name]
            attr_msg = message.struct_value.add()
            if attr.type == CompSpecMsg.TYPE_ENUM:
                PyValue2PbEnum(attr_msg, attr, curr_value)
            elif attr.type == CompSpecMsg.TYPE_SCALAR:
                PyValue2PbScalar(attr_msg, attr, curr_value)
            elif attr.type == CompSpecMsg.TYPE_STRING:
                PyString2PbString(attr_msg, attr, curr_value)
            elif attr.type == CompSpecMsg.TYPE_VECTOR:
                PyList2PbVector(attr_msg, attr, curr_value)
            elif attr.type == CompSpecMsg.TYPE_STRUCT:
                sub_attr = FindSubStructType(pb_spec, attr.predefined_type)
                if sub_attr:
                    PyDict2PbStruct(attr_msg, sub_attr, curr_value)
                else:
                    logging.error("PyDict2PbStruct: substruct not found.")
                    return None
            elif attr.type == CompSpecMsg.TYPE_UNION:
                sub_attr = FindSubStructType(pb_spec, attr.predefined_type)
                if sub_attr:
                    PyDict2PbUnion(attr_msg, sub_attr, curr_value)
                else:
                    logging.error("PyDict2PbStruct: subunion not found.")
                    return None
            else:
                logging.error("PyDict2PbStruct: unsupported type %s",
                              attr.type)
                return None
        else:
            # TODO: instead crash the test, consider to generate default value
            # in case not provided in the py_value.
            logging.error("PyDict2PbStruct: attr %s not provided", attr.name)
            return None
    if len(provided_attrs) > 0:
        logging.error("PyDict2PbStruct: provided dictionary included elements" +
                      " not part of the type being converted to: %s",
                      provided_attrs)
        return None
    return message


def PyDict2PbUnion(message, pb_spec, py_value):
    """Converts Python dict to VTS VariableSecificationMessage (union).

    Args:
        pb_spec: VariableSpecificationMessage which captures the
                 specification of a target attribute.
        py_value: A dictionary that represents a struct.

    Returns:
        Converted VariableSpecificationMessage if found, None otherwise
    """
    if len(py_value) > 1:
        logging.error("PyDict2PbUnion: Union only allows specifying " +
                      "at most one field. Current Python dictionary " +
                      "has size %d", len(py_value))
        return None

    if pb_spec.name:
        message.name = pb_spec.name
    message.type = CompSpecMsg.TYPE_UNION
    provided_attrs = set(py_value.keys())
    for attr in pb_spec.union_value:
        # Since it is a union type, we stop after finding one field name
        # that matches, and shouldn't throw an error when name is not found.
        if attr.name in py_value:
            provided_attrs.remove(attr.name)
            curr_value = py_value[attr.name]
            attr_msg = message.union_value.add()
            if attr.type == CompSpecMsg.TYPE_ENUM:
                PyValue2PbEnum(attr_msg, attr, curr_value)
            elif attr.type == CompSpecMsg.TYPE_SCALAR:
                PyValue2PbScalar(attr_msg, attr, curr_value)
            elif attr.type == CompSpecMsg.TYPE_STRING:
                PyString2PbString(attr_msg, attr, curr_value)
            elif attr.type == CompSpecMsg.TYPE_VECTOR:
                PyList2PbVector(attr_msg, attr, curr_value)
            elif attr.type == CompSpecMsg.TYPE_STRUCT:
                # TODO: is a nested struct in union stored in sub_union field.
                sub_attr = FindSubUnionType(pb_spec, attr.predefined_type)
                if sub_attr:
                    PyDict2PbStruct(attr_msg, sub_attr, curr_value)
                else:
                    logging.error("PyDict2PbStruct: substruct not found.")
                    return None
            elif attr.type == CompSpecMsg.TYPE_UNION:
                sub_attr = FindSubUnionType(pb_spec, attr.predefined_type)
                if sub_attr:
                    PyDict2PbUnion(attr_msg, sub_attr, curr_value)
                else:
                    logging.error("PyDict2PbUnion: subunion not found.")
                    return None
            else:
                logging.error("PyDict2PbStruct: unsupported type %s",
                              attr.type)
                return None
        else:
            # Add a field, where name field is initialized as an empty string.
            # In generated driver implementation, driver knows this field is
            # not used, and skip reading it.
            message.union_value.add()

    if len(provided_attrs) > 0:
        logging.error("PyDict2PbUnion: specified field is not in the union " +
                      "definition for union type %s", provided_attrs)
        return None
    return message


def Convert(pb_spec, py_value):
    """Converts Python native data structure to VTS VariableSecificationMessage.

    Args:
        pb_spec: VariableSpecificationMessage which captures the
                 specification of a target attribute.
        py_value: Python value provided by a test case.

    Returns:
        Converted VariableSpecificationMessage if found, None otherwise
    """
    if not pb_spec:
        logging.error("py2pb.Convert: ProtoBuf spec is None", pb_spec)
        return None

    message = CompSpecMsg.VariableSpecificationMessage()
    message.name = pb_spec.name

    if isinstance(py_value, CompSpecMsg.VariableSpecificationMessage):
        message.CopyFrom(py_value)
    elif pb_spec.type == CompSpecMsg.TYPE_STRUCT:
        PyDict2PbStruct(message, pb_spec, py_value)
    elif pb_spec.type == CompSpecMsg.TYPE_UNION:
        PyDict2PbUnion(message, pb_spec, py_value)
    elif pb_spec.type == CompSpecMsg.TYPE_ENUM:
        PyValue2PbEnum(message, pb_spec, py_value)
    elif pb_spec.type == CompSpecMsg.TYPE_SCALAR:
        PyValue2PbScalar(message, pb_spec, py_value)
    elif pb_spec.type == CompSpecMsg.TYPE_STRING:
        PyString2PbString(message, pb_spec, py_value)
    elif pb_spec.type == CompSpecMsg.TYPE_VECTOR:
        PyList2PbVector(message, pb_spec, py_value)
    else:
        logging.error("py2pb.Convert: unsupported type %s",
                      pb_spec.type)
        return None

    return message