import argparse
import inspect
import json
import sys

from net.happygears.proto.NSG_pb2 import DataSource
from scripts import var_builder

import nsg
from net.happygears.nsgpython.apps.impl import VariableAppResponseConverter
from net.happygears.nsgpython.common import PVarReporter

FORMATS = ['json', 'report']


def find_class():
    """
    inspect imported module `tags` to find and return class that has method `execute`
    """
    klasses = [x[1] for x in inspect.getmembers(var_builder, inspect.isclass) if hasattr(x[1], 'execute')]
    if not klasses:
        return None
    return klasses[0]


def init():
    """
    entry point for initialization. Find class defined in user's script var_builder.py,
    then create and return an instance. Note that if the class reads some files
    in its constructor, this will happen here.

    :return instance of the class defined in var_builder.py
    """
    klass = find_class()
    if klass is None:
        raise RuntimeError(
            'Can not find suitable class in module "var_builder": expected to find class with method "execute()"')
    return klass(nsg.get_log())


def run(user_class_object, device):
    """
    entry point for script var_builder.py. NSG servers and tests call this function to run this script
    We do not need to do anything here at this time but it is still good to have a point where
    we can insert some code in the future without making any changes in user's scripts

    Python dictionary generated by the script contains references to
    device component objects (PyNetworkInterface, PyHardwareComponent etc) which can be used
    only while we have a reference to the device. Calls to this function come from `main_variables()`
    and from internal classes in nsg-python-gw, such as grpc server. It does not make sense to
    duplicate code that converts dictionary to `VariablesAppResponse` in many places, it is much
    better to do it once. The drawback is that if the user tries to print or inspect generated
    response, it will look differently from the dictionary their script builds.

    This problem is unique to this type of Python app because other apps do not operate with references
    to internal NSG objects. However, it probably makes sense to convert data to response protobuf
    types in corresponding `run_*` function for all apps just for the sake of consistency.

    :param user_class_object    instance of the class defined in user's module tags.py
    :param device   input device (PyDevice)
    :return dictionary with variable definitions
    """
    # var_builder.execute() returns dictionary where the key is variable name and value is
    # a dict that describes the variable
    if user_class_object is None:
        print('Parameter user_class_object can not be None', file=sys.stderr)
        return None
    variables = user_class_object.execute(device)
    for var_name, var_instances in variables.items():
        for d in var_instances:
            if 'component' in d:
                comp = d['component']
                d['name'] = comp.name
                d['index'] = comp.index
                d['scale'] = comp.sensor_scale
                if 'type' not in d:
                    d['type'] = comp.data_type
                del d['component']  # delete ref.to component b/c otherwise it does not serialize to json
    return variables


def json_response_converter(variables):
    for var_name, var_instances in variables.items():
        for d in var_instances:
            d['dsc'] = DataSource.Name(d['dsc'])
    return json.dumps(variables, cls=nsg.ExtendedTypeEncoder, sort_keys=True, indent=2,
                      separators=(',', ': '))


def script_run(infile, outfile, format):
    input_device = nsg.deserialize_device(infile)
    user_class_object = init()
    variables_dict = run(user_class_object, input_device)
    if format == 'json':
        """
        TODO: Add explanation for the change.
        DataSource enum in NSG.proto
        """
        output = json_response_converter(variables_dict)
    if format == 'report':
        response = VariableAppResponseConverter.convert(1, variables_dict)  # <<< Java class
        reporter = PVarReporter(input_device)  # <<< Java class
        reporter.add(response)
        output = reporter.getContents()

    if outfile is None or outfile == '-':
        sys.stdout.write(output)
    else:
        with open(outfile, 'w') as fw:
            fw.write(output)


def main(argv=None):
    # parse cli arguments and deserialize device.
    # when there are no cli parameters, assume the script is loaded by nsg-python-gw which is
    # going to call run() directly
    if not argv or len(argv) <= 1:
        return
    parser = argparse.ArgumentParser(description='NetSpyGlass polling variable builder Python application')
    parser.add_argument('input_device', nargs='+',
                        help='the file name where input device is stored in json format')
    parser.add_argument('-f', '--format', choices=FORMATS, default='json',
                        help='use this format to generate output (default: json)')
    parser.add_argument('-o', '--output',
                        help='the file name where the result should be stored in json format; '
                             'if missing or equal to "-", use stdout')

    args = parser.parse_args()

    infiles = args.input_device
    outfile = args.output
    if not infiles:
        print('Missing input file', file=sys.stderr)
        parser.print_help()
        return 1
    for infile in infiles:
        if not infile:
            continue
        if infile == outfile:
            print('Output file can not be the same as input file', file=sys.stderr)
            parser.print_help()
            return 1
        input_device = nsg.deserialize_device(infile)
        user_class_object = init()
        variables_dict = run(user_class_object, input_device)
        if args.format == 'json':
            output = json_response_converter(variables_dict)
        if args.format == 'report':
            response = VariableAppResponseConverter.convert(1, variables_dict)
            reporter = PVarReporter(input_device)
            reporter.add(response)
            output = reporter.getContents()

        if outfile is None or outfile == '-':
            sys.stdout.write(output)
        else:
            with open(outfile, 'w') as fw:
                fw.write(output)


if __name__ == "__main__":
    main(sys.argv)
