#!/usr/bin/python3
"""
@file ifopcfilecheck.py
@copyright ESO - European Southern Observatory
@defgroup meltcs	
@brief IFTEST - Subsystem Interface Testing
Tool to verify correctness of opcua entries in a IFTEST yaml file. 
"""

import argparse
import asyncio
from asyncua import Client

import yaml
from yaml.loader import BaseLoader

def find_system_id(yaml_dict):
    """
    Derives the system IF from the yaml contents
    Reads the first entry of the yaml data and returns
    the result of comparing with a fingerprint.
    """
    tmp = yaml_dict.get(next(iter(yaml_dict))).get('data_entity')
    if 'ModM2' in tmp:
        id = 'M2'
    elif 'ModPfsa' in tmp:
        id = 'Pfsa'
    elif 'ModM5' in tmp:
        id = 'M5'
    else:
        id = 'UNKNOWN'
    return id

def print_results(msg,data_set):
    """
    Prints final message to user with summary of results
    """
    print('{}: {}'.format(msg,len(data_set)))
    if len(data_set) > 0:
        for i in data_set:
            print('\t{}'.format(i))

def get_type_name(type_id):
    """
    Returns type name from given ID
    """
    switcher={
        'i=1' :'BOOLEAN',
        'i=3' :'BYTE',
        'i=11':'DOUBLE',
        'i=4' :'INT16',
        'i=5' :'UINT16',
        'i=6' :'INT32',
        'i=7' :'UINT32',
        'i=10':'FLOAT',
        'i=12':'STRING'
    }
    return switcher.get(type_id)

async def main():
    description_text = """
    Tool to verify correctness of opcua entries in a IFTEST yaml file.
    \rGets connected to the OPC-UA server specified in the YAML file and checks
    \rthe validity of the contents against the data in the server.
    """

    example_text = """
    example:
    ifopcuafilecheck iftest_m2.yaml
    """

    # Instantiate the parser
    parser = argparse.ArgumentParser(description=description_text,
                                     epilog=example_text,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)

    parser.add_argument('file_name', type=str, help='Path and name of dump yaml file.')
    parser.add_argument('--verbose', action='store_true', help='Verbose mode.')

    # Parse input arguments
    args      = parser.parse_args()
    file_name = args.file_name
    verbose   = args.verbose

    # Full path YAML file must be provided
    with open(file_name) as f:
        # Look first for duplicated entries
        temp = set()
        duplicated = set()
        for line in f:
            line = line.lstrip(' ').rstrip(' \n')
            if line.startswith('#') or line.isspace():
                continue
            if line.startswith('members') or line.startswith('data_entity') or line.startswith('method'):
                continue
            if line.endswith(':') and line in temp:
                duplicated.add(line)
            else:
                temp.add(line)

        # Raises an error and exit in case of duplicated
        # entries and exit
        if len(duplicated) > 0:
            print('DUPLICATED:')
            for i in duplicated:
                print('\t{}'.format(i))
            exit('\nERROR: Duplicated entries must be fixed first.')

        # Load YAML contents from rpc and attribute sections
        # in two separate dictionaries
        f.seek(0)
        yaml_data = yaml.load(f,Loader=BaseLoader)
        yaml_rpc = yaml_data.get('rpc')
        yaml_att = yaml_data.get('attributes')
        yaml_grp = yaml_data.get('groups')

        # Identify System based on Data Entity
        system_id = find_system_id(yaml_att)

    # Extract server address from yaml data and browse the server
    opcua_server = yaml_data.get('opcua').get('server_address')
    print('Browsing {} ...'.format(opcua_server))

    failed_nodes = set()
    failed_type = set()
    failed_arguments = set()
    failed_attributes_in_groups = set()

    client = Client(opcua_server)

    try:
        await client.connect()
    except:
        exit("Error connecting to server {}".format(opcua_server))

    try:
        for attribute in yaml_att:
            if verbose:
                print('-----\nATTRIBUTE: {}'.format(attribute))

            yaml_att_path = yaml_att.get(attribute).get('path')
            yaml_att_entity = yaml_att.get(attribute).get('data_entity')

            # Replace special characters
            yaml_att_path = yaml_att_path.replace('%5B','[').replace('%5D',']')
            yaml_att_path = yaml_att_path.replace('/#','ns=')

            try:
                # Test if identifier is numeric
                int(yaml_att_path.split(',')[1])
                yaml_att_path = yaml_att_path.replace(',',';i=')
            except:
                # Identifier is string
                yaml_att_path = yaml_att_path.replace(',',';s=')

            var = client.get_node(yaml_att_path)

            # Check validity of the node path
            try:
                await var.read_browse_name()
            except:
                # An exception is raised when the given path is
                # non-existent
                failed_nodes.add(attribute)
                if verbose:
                    print('FAILED NODE: {}'.format(yaml_att_path))
                continue

            # In the case of a scalar an exception is raised when
            # trying to get array dimensions
            try:
                array_dimensions = await var.read_array_dimensions()
                if array_dimensions is None: # Some versions of the OPCUA server
                    array_dimensions = [0]
            except:
                array_dimensions = [0] # Means scalar

            if len(array_dimensions) == 1:
                # Case for scalars and arrays
                size = array_dimensions[0]
            else:
                # Case for Matrix
                size = -1

            # Cases of wrong data entity declared for this attribute
            if (size == -1 and 'matrix' not in yaml_att_entity.lower()) or \
               (size >  -1 and 'matrix' in     yaml_att_entity.lower()) or \
               (size >   0 and 'vector' not in yaml_att_entity.lower()) or \
               (size <   1 and 'vector' in     yaml_att_entity.lower()):
                failed_type.add(attribute)
                if verbose:
                    print('FAILED SIZE: {}'.format(yaml_att_entity))

            # Get Data type for the attribute in the server and compare it
            # with the data entity in the yaml file
            variant_type = str(await var.read_data_type_as_variant_type()).split('.')[1]
            type_str = system_id + variant_type

            # The verification failes when the type from the
            # server is not found in the entity from the yaml
            if type_str.lower() not in yaml_att_entity.lower():
                failed_type.add(attribute)
                if verbose:
                    print('FAILED TYPE: {} does not match {}'.format(yaml_att_entity,type_str))

        for rpc in yaml_rpc:
            if verbose:
                print('-----\nRPC: {}'.format(rpc))

            yaml_rpc_path = yaml_rpc.get(rpc).get('path')

            # Replace specifics for RPC
            yaml_rpc_path = yaml_rpc_path.replace('%23RPC','#RPC')
            yaml_rpc_path = yaml_rpc_path[:yaml_rpc_path.find(',')] + yaml_rpc_path[yaml_rpc_path.rfind(','):]
            yaml_rpc_path = yaml_rpc_path.replace('/#','ns=').replace(',',';s=')

            var = client.get_node(yaml_rpc_path)

            # Check validity of the node path
            try:
                await var.read_browse_name()
            except:
                failed_nodes.add(rpc)
                if verbose:
                    print('FAILED NODE: {}'.format(yaml_rpc_path))
                continue

            # The InputArgument attribute of the RPC contain as data
            # the required arguments in nested arrays
            var_arguments = client.get_node(yaml_rpc_path + '.InputArguments')
            input_array_dimensions = await var_arguments.read_array_dimensions()
            var_type_list = []
            for i in range(input_array_dimensions[0]):
                value = await var_arguments.get_value()

                if len(value[i].ArrayDimensions) == 0:
                    size = 1 # scalar
                else:
                    size = value[i].ArrayDimensions.pop()

                # The type for this argument is appended to the final list
                if get_type_name(value[i].DataType.to_string()) != None:
                    var_type_list = var_type_list + [get_type_name(value[i].DataType.to_string()).lower()] * size

            yaml_rpc_arguments = yaml_rpc.get(rpc).get('arguments')
            if yaml_rpc_arguments is None:
                # Case when no arguments are present
                yaml_rpc_arguments = ''

            yaml_rpc_arguments = yaml_rpc_arguments.split()
            argument_type_list = []
            for i in yaml_rpc_arguments:
                if '[' in i:
                    argument_split = i.split('[')
                    #print('argument_split: {}'.format(argument_split))
                    argument_type = argument_split[0]
                    argument_size = argument_split[1].rstrip(']')
                else:
                    argument_type = i
                    argument_size = 1

                # The type found in the argument is appended to the final list
                argument_type_list = argument_type_list + [argument_type.lower()] * int(argument_size)

            # Both argument lists must match to verify ours correct
            if argument_type_list != var_type_list:
                failed_arguments.add(rpc)
                if verbose:
                    print('FAILED ARGUMENTS: {}'.format(rpc))
                    print('YAML:   {}'.format(argument_type_list))
                    print('SERVER: {}'.format(var_type_list))

        for group in yaml_grp:
            if verbose:
                print('-----\nGROUP: {}'.format(group))
            
            # Get the full list of attributes from the group
            yaml_grp_att_list = yaml_grp.get(group).split()

            # Check that every att in the list is an actual attribute
            for grp_att in yaml_grp_att_list:
                if grp_att not in yaml_att:
                    failed_attributes_in_groups.add(grp_att)
                    if verbose:
                        print("FAILED ATTRIBUTE IN GROUP: {}".format(grp_att))

    finally:
        await client.disconnect()

    print('\nSUMMARY')
    print('-------')
    print_results("Invalid nodes (check path)",failed_nodes)
    print_results("\nPossibly wrong type (check data_entity)",failed_type)
    print_results("\nPossibly wrong arguments (check arguments)",failed_arguments)
    print_results("\nPossibly nonexisting attributes in groups",failed_attributes_in_groups)

if __name__ == "__main__":
    asyncio.run(main())

