from pprint import pformat
import collections
from .config import *
from .jinjafilters import *
try:
    from jinja2 import Environment, FileSystemLoader
except:
    print("missing jinja2:")
    print ("Trying to Install required module: jinja2")
    os.system('python3 -m pip install jinja2')


def get_schema_from_method_data(path_data, method, key):
    """
    Fetches the list of schemas available in Swagger json data
    :param method_data: Method data
    :param key: Responses/Parameters
    :return: File Name
    """
    method_data = dict_find_item(path_data, method)
    if method_data:
        resp_data = dict_find_item(method_data, key)

        if resp_data:
            if key == 'parameters':
                for param in resp_data:
                    if 'in' in param and param['in'] == 'body':
                        schema_data = dict_find_item(param, "schema")
                        if schema_data:
                            return schema_data
                pass
            elif key == 'responses':
                for resp_code, resp_code_data in resp_data.items():
                    schema_data = dict_find_item(resp_code_data, "schema")
                    if schema_data:
                        return schema_data
    return None


def get_schema_info_from_method(path_data, method):
    param_schema_file_name = None
    resp_schema_file_name = None
    param_schema_data = get_schema_from_method_data(path_data, method, 'parameters')
    resp_schema_data = get_schema_from_method_data(path_data, method, 'responses')
    if param_schema_data and '$ref' in param_schema_data:
        param_schema_file_name = param_schema_data['$ref'].split('#')[0]
    if resp_schema_data and '$ref' in resp_schema_data:
        resp_schema_file_name = resp_schema_data['$ref'].split('#')[0]

    return param_schema_data, param_schema_file_name, resp_schema_data, resp_schema_file_name


def get_method_body_params_data(path_data, method):
    """Fetches the param data from Swagger URI Path data, for a given method

    Param data consists of the 'schema', is handled differently.

    :param path_data: Swagger URI Path data
    :param method: Method name (get, post)
    :return: File Name
    """
    method_data = dict_find_item(path_data, method)
    if method_data:
        param_data = dict_find_item(method_data, 'parameters')
        if param_data:
            for param in param_data:
                if 'in' in param and param['in'] == 'body' and not 'schema' in param:
                    return param
    return None


class JsonObject:
    """
    Json Object Processing Class
    """
    name = ""
    for_py_client = False
    headers = []
    props = {}

    def __init__(self, template_dir, out_dir):
        self.logger = logging.getLogger("JsonObject")
        self.out_dir = out_dir
        self.template_dir = template_dir
        self.env = None

    def set_props(self, props):
        self.props = collections.OrderedDict(sorted(props.items(),
                        key=lambda item: item[0]))

    def set_headers(self, props):
        self.headers = []
        self.logger.debug("set_headers")
        for prop, prop_data in props.items():
            json_type = get_json_type(prop_data)
            self.logger.debug("Prop: %s Json Type: %s", prop, json_type)
            if json_type == 'array':
                if prop_data['items']['type'] == 'object':
                    temp = class_name_syntax(prop_data['items']['name']) + 'Attr.h'
                    self.headers.append(temp)
            elif json_type in ["object"]:
                temp = class_name_syntax(prop_data['name']) + 'Attr.h'
                self.headers.append(temp)
        if len(self.headers) > 1:
            self.logger.debug(self.headers)
        else:
            self.logger.debug("No Headers required")

    def get_json_obj_deps(self):
        self.deps = []
        self.logger.debug("get_json_obj_deps")
        for prop, prop_data in self.props.items():
            json_type = get_json_type(prop_data)
            self.logger.debug("Prop: %s Json Type: %s", prop, json_type)
            if json_type == 'array':
                if prop_data['items']['type'] == 'object':
                    self.deps.append(class_name_syntax(prop_data['items']['name']))
            elif json_type in ["object"]:
                self.deps.append(class_name_syntax(prop_data['name']))
        return self.deps

    def create_resource_header(self):
        file_name = class_name_syntax(self.name) + "Attr.h"
        self.logger.debug("Creating header file %s", file_name)
        template_h_env = self.env.get_template("IoTResourceAttributes.h.jinja2")
        header_text = template_h_env.render(schema_data=self)
        write_file(file_name, header_text, self.out_dir)

    def create_jinja_env(self):
        self.env = Environment(loader=FileSystemLoader(self.template_dir),
                          trim_blocks=True,
                          lstrip_blocks=False)
        self.env.filters['all_caps'] = all_caps
        self.env.filters['class_name_syntax'] = class_name_syntax
        self.env.filters['enum_name_syntax'] = enum_name_syntax
        self.env.filters['convert_to_cpp_return_type'] = convert_to_cpp_return_type
        self.env.filters['convert_to_cpp_type'] = convert_to_cpp_type
        self.env.filters['default_value_objc_type'] = default_value_objc_type
        self.env.filters['convert_to_objc_return_type'] = convert_to_objc_return_type
        self.env.filters['convert_to_objc_type'] = convert_to_objc_type
        self.env.filters['convert_to_objc_alloc_type'] = convert_to_objc_alloc_type
        self.env.filters['variable_syntax'] = variable_syntax
        self.env.globals['is_prop_an_object'] = is_prop_an_object
        self.env.globals['is_prop_an_object_array'] = is_prop_an_object_array
        self.env.globals['get_object_name_from_obj_array'] = get_object_name_from_obj_array
        self.env.globals['get_objc_object_name_from_obj_array'] = get_objc_object_name_from_obj_array
        self.env.globals['get_objc_alloc_object_name_from_obj_array'] = get_objc_alloc_object_name_from_obj_array
        self.env.globals['pack_objc_type'] = pack_objc_type
        self.env.globals['unpack_objc_type'] = unpack_objc_type
        self.env.globals['objc_property_qualifier'] = objc_property_qualifier

    def generate(self):
        self.create_jinja_env()
        self.create_resource_header()


class Schema:
    """
    Schema Parser
    """
    # If set, generates Resource Attribute classes in a single file, else in different files
    single_file = True

    # Template(Jinaj2) dir, Schema dir(swaggers and schema json) and Out dir(where the auto generated code is)
    template_dir = None
    out_dir = None
    schema_dir = None

    # Properties list of dicts - used for resourceattrstring.h creation
    prop_obj = None

    # Recursion variable - testing
    temp_objects = []
    depth = 0

    # List of JSON objects stored in the class JsonObject
    json_objects = []

    # List of processed files
    processed_files = []
    processed_obj_names = []

    def __init__(self, prop_obj):
        self.logger = logging.getLogger("Schema")

        self.single_file = swagger_config['single_file']
        self.template_dir = swagger_config['template_dir']
        self.out_dir = swagger_config['out_dir']
        self.schema_dir = swagger_config['schema_dir']

        self.prop_obj = prop_obj

        # Populate the list of JSON objects
        for directory in self.schema_dir:
            print("Processing Schemas in: " + directory)
            self.populate_json_objects(directory)

    def populate_json_objects(self, directory):
        """Fetches the JSON objects from schemas and swaggers

        JSON Objects could be present in,
        1. simple schema files
        2. Swagger files, in parameters or responses section
        """
        # Parse all the JSON files, first
        files = get_dir_list(directory, ".json")
        for file in files:
            if "swagger.json" not in file and "restype.json" not in file:
                self.process_simple_json(file, directory)

        # Parse the Swaggers, to create attributes classes
        files = get_dir_list(directory, ".swagger.json")
        for file in files:
            self.process_swagger(file, directory)

    def process_simple_json(self, file, directory):
        """"For a given JSON file, processes all the JSON objects

        The JSON key definitions defines the object properties it contains. This can have
        nested JSON objects. This extracts each JSON object, from which we create CPP classes for it
        """
        json_ref_dict, json_dict = load_json_schema(file, True, directory)
        try:
            self.logger.info("process_simple_json: --------------> Processing schema %s", file)
            self.processed_files.append(file)
            json_data = json_ref_dict['definitions']
            self.process_nested_json_obj(json_data)
        except Exception as e:
            self.logger.error("Exception %s", e)

    def process_swagger(self, file, directory):
        """Extract the JSON object present in Swagger files

        The JSON object could be present inside Swagger file as well.
        1. Inside the CRUD methods, referenced as schemas
        2. Inside the CRUD parameters section, with param type in body.

        This function extracts the JSON objects present in either of them.
        """
        self.logger.info("process_swagger: --------------> Processing swagger %s", file)
        swagger_reffed_json, swagger_json = load_json_schema(file, True, directory)
        self.process_swagger_schemas(file, swagger_reffed_json, swagger_json)
        self.process_swagger_param_objects(file, swagger_reffed_json, swagger_json)

    def process_swagger_schemas(self, file, swagger_reffed_json, swagger_json):
        """Extracts JSON Object present in the Schema ref tags of the CRUD data

        For each Swagger URI path
        1. Fetch the CRUD data
        2. From the CRUD data, get schemas ref'ed in parameters and responses section
        """
        self.logger.info("process_swagger_schemas: --------------> Processing swagger %s for schemas", file)
        for path, path_data in swagger_json['paths'].items():
            for method, method_data in path_data.items():
                param_schema_data, param_schema_file_name, resp_schema_data, resp_schema_file_name = get_schema_info_from_method(path_data, method)

                self.process_schema_data('parameters', swagger_reffed_json, path,
                                         method, file, param_schema_file_name, param_schema_data)
                self.process_schema_data('responses', swagger_reffed_json, path,
                                         method, file, resp_schema_file_name, resp_schema_data)

    def process_schema_data(self, schema_in, swagger_reffed_json, path, method, file, schema_file, schema_data):
        """Helper function for process_swagger_schemas

        Process the local refs, used by schemas
        """
        self.logger.debug("process_schema_data: START for %s", schema_in)
        self.logger.debug("process_schema_data: schema data for %s schema %s path %s method %s file --> %s", schema_file, path,
                          method, file, pformat(schema_data))
        if schema_data and not self.is_schema_already_processed(schema_file):
            schema_data = get_schema_from_method_data(swagger_reffed_json['paths'][path], method, schema_in)
            self.logger.info("process_schema_data: Reffed data %s", pformat(schema_data))
            if 'name' in schema_data and schema_data['name'].lower() in self.processed_obj_names:
                self.logger.info("process_schema_data: Already processed %s in %s file", schema_data['name'], file)
            else:
                self.process_nested_json_obj({'data': schema_data})
        else:
            self.logger.info("process_schema_data: Nothing to Process for %s path %s method %s file", path, method, file)
        self.logger.debug("process_schema_data: END for %s", schema_in)

    def is_schema_already_processed(self, schema_file_name):
        """Checks if the schema file is already processed or not by process_simple_json

        Ideally, all the schemas file in the schema_dir would be processed by process_simple_json, only the
        local refs with in a file won't be procssed by process_simple_json. These should be processed separately
        """
        self.logger.info("is_schema_already_processed %s file", schema_file_name)
        if schema_file_name:
            if schema_file_name in self.processed_files:
                self.logger.info("Already Processed %s file ", schema_file_name)
            else:
                self.logger.error("NOT Processed %s file, should not happen", schema_file_name)
            return True
        else:
            # This should be a ref in the local file
            self.logger.info("This schema is local ref")
            return False

    def process_swagger_param_objects(self, file, swagger_reffed_json, swagger_json):
        """Extracts JSON Objects from CRUD Parameter section, which are of type body

        Fetches the JSON Object data from the parameter section and checks for nested
        JSON Objects in it. The extracted list is appended to self.json_objects, for futher processing
        """
        self.logger.info("process_swagger_param_objects: --------------> Processing swagger %s for param objects", file)
        for path, path_data in swagger_reffed_json['paths'].items():
            for method, method_data in path_data.items():
                param_data = get_method_body_params_data(path_data, method)
                if param_data:
                    if 'name' in param_data:
                        self.logger.info("process_swagger_param_objects: Param data %s\n Processed Object names %s\n",
                                         pformat(param_data), pformat(self.processed_obj_names))
                        if not param_data['name'].lower() in self.processed_obj_names:
                            self.process_nested_json_obj({'data': param_data})
                        else:
                            self.logger.info("process_swagger_param_objects: Already Processed\
                            %s path %s method %s object %s file", path, method, param_data['name'], file)

    def process_nested_json_obj(self, json_data):
        """Create objects of class JsonObject, for each JSON object in the Schema

        On the input JSON data, json_object_generator is called, which runs in a recursive manner
        to extract all the nested objects from the input JSON data.

        Once the nested JSON objects are extracts, each of them is stored in `~JsonObject`
        """
        self.temp_objects = []
        self.depth = 0
        self.json_object_generator(json_data)
        for data in self.temp_objects:
            json_obj = JsonObject(self.template_dir, self.out_dir)
            json_obj.data = data
            json_obj.set_props(data['properties'])
            self.prop_obj.add_props(data['properties'])
            json_obj.set_headers(data['properties'])
            json_obj.name = strip_name_convention(data['name'])
            if not json_obj.name.lower() in self.processed_obj_names:
                self.processed_obj_names.append(json_obj.name.lower())
                self.json_objects.append(json_obj)
            else:
                self.logger.error("%s Already present in json_objects ", json_obj.name)

    def fix_json_obj_dependency(self):
        """ Fix the order for class generation

        By defining all resource attribute classes in a single file, we end up getting the CPP forward
        declaration errors, as some of the classes reference other class API, before the actual implementation.
        """
        import networkx as nx
        #import pygraphviz as pgv
        g = nx.OrderedDiGraph()
        for json_obj in self.json_objects:
            g.add_node(class_name_syntax(json_obj.name))
            deps = json_obj.get_json_obj_deps()
            if len(deps) > 0:
                for dep in deps:
                    g.add_edge(class_name_syntax(json_obj.name), dep)
        #nx.nx_pydot.write_dot(g, 'deps.dot')
        post_order = list(nx.dfs_postorder_nodes(g))
        self.logger.info("fix_json_obj_dependency : post order %s", post_order)
        self.logger.info("fix_json_obj_dependency: after removing dups %s", remove_list_duplicates(post_order))
        return remove_list_duplicates(post_order)

    def json_object_generator(self, json_data, readOnly = False):
        """For a given JSON Object, recursively extracts all the JSON Objects in it

        JSON Object, might have nested objects inside or might have Array of Objects. This
        function iterates over the input JSON Object, to find out
        1. If any object is present, then run json_object_generator, this function, again on that
        2. If it is an array, and the contents of it is an Object, then run recursively

        self.depth is just a debugging variable, and doesn't serve any other purpose
        """
        self.depth += 1
        self.logger.debug("json_object_generator: START --> %s", self.depth)
        for k, v in json_data.items():
            self.logger.debug("Key is %s, type is %s", k, v['type'])
            self.logger.debug(pformat(v))

            if 'readOnly' in v:
                readOnly = v['readOnly'] or readOnly
            if v['type'] == 'object':
                self.logger.info("Adding to object %s list", v['name'])
                self.temp_objects.append(v)
                self.logger.debug("Processing %s properties", k)

                if readOnly:
                    for propKey, propValue in v['properties'].items():
                        self.logger.debug("Setting readonly for child %s", propKey)
                        propValue['readOnly'] = True
                
                self.json_object_generator(v['properties'], readOnly)
            elif v['type'] == 'array':
                self.logger.debug("%s is an array", k)
                if is_prop_an_object_array(v):
                    temp = dict()
                    temp['items'] = v['items']
                    self.logger.debug("Processing %s items", k)
                    self.json_object_generator(temp, readOnly)
                else:
                    self.logger.debug("%s has no object in array", k)
        self.logger.debug("json_object_generator: END--> %s", self.depth)
        self.depth -= 1

    def create_jinja_env(self):
        self.env = Environment(loader=FileSystemLoader(self.template_dir),
                          trim_blocks=True,
                          lstrip_blocks=False)
        self.env.filters['all_caps'] = all_caps
        self.env.filters['class_name_syntax'] = class_name_syntax
        self.env.filters['enum_name_syntax'] = enum_name_syntax
        self.env.filters['convert_to_cpp_return_type'] = convert_to_cpp_return_type
        self.env.filters['convert_to_cpp_type'] = convert_to_cpp_type
        self.env.filters['convert_to_objc_return_type'] = convert_to_objc_return_type
        self.env.filters['convert_to_objc_type'] = convert_to_objc_type
        self.env.filters['convert_to_objc_alloc_type'] = convert_to_objc_alloc_type
        self.env.filters['default_value_objc_type'] = default_value_objc_type
        self.env.filters['variable_syntax'] = variable_syntax
        self.env.filters['string_constant'] = string_constant
        self.env.filters['class_data_member'] = class_data_member
        self.env.filters['class_data_member_with_prefix_suffix'] = class_data_member_with_prefix_suffix
        self.env.filters['local_variable'] = local_variable
        self.env.globals['is_prop_an_object'] = is_prop_an_object
        self.env.globals['is_prop_an_object_array'] = is_prop_an_object_array
        self.env.globals['props_have_object_array'] = props_have_object_array
        self.env.globals['props_have_writable_object_array'] = props_have_writable_object_array
        self.env.globals['is_prop_array_type'] = is_prop_array_type
        self.env.globals['is_prop_string_type'] = is_prop_string_type
        self.env.globals['get_object_name_from_obj_array'] = get_object_name_from_obj_array
        self.env.globals['get_objc_object_name_from_obj_array'] = get_objc_object_name_from_obj_array
        self.env.globals['get_objc_alloc_object_name_from_obj_array'] = get_objc_alloc_object_name_from_obj_array
        self.env.globals['pack_objc_type'] = pack_objc_type
        self.env.globals['unpack_objc_type'] = unpack_objc_type
        self.env.globals['objc_property_qualifier'] = objc_property_qualifier
 

    def create_resource_pybind(self):
        self.create_jinja_env()
        file_name = "resource_pybind.h"
        self.logger.debug("Creating Resource Pybind file %s", file_name)
        template_cpp_env = self.env.get_template("resource_attribute_pybind.h.jinja2")
        cpp_text = template_cpp_env.render(schema_data=self.json_objects)
        write_file(file_name, cpp_text, self.out_dir)

    def create_resource_attr_in_single_file(self, for_py_client):
        self.create_jinja_env()
        if for_py_client:
            file_name = "resource_attributes_py_client.h"
        else:
            file_name = "IoTResourceAttributes.h"
        self.logger.debug("Creating Resource Attributes file %s", file_name)
        template_env = self.env.get_template("IoTResourceAttributes.h.jinja2")
        dep_list = self.fix_json_obj_dependency()
        dep_json_objects = []

        if for_py_client:
            for json_object in self.json_objects:
                json_object.for_py_client = True

        for dep in dep_list:
            for json_object in self.json_objects:
                if class_name_syntax(json_object.name) == dep:
                    dep_json_objects.append(json_object)
        text = template_env.render(schema_objects=dep_json_objects)
        write_file(file_name, text, self.out_dir)

        file_name = "IoTResourceAttributes.mm"
        template_objc_env = self.env.get_template("IoTResourceAttributes.mm.jinja2")
        objc_text = template_objc_env.render(schema_objects=dep_json_objects)
        write_file(file_name, objc_text, self.out_dir)

        file_name = "IoTTypes.h"
        template_objc_env = self.env.get_template("IoTTypes.h.jinja2")
        objc_text = template_objc_env.render(schema_objects=dep_json_objects)
        write_file(file_name, objc_text, self.out_dir)


    def process(self):
        if self.single_file:
            self.json_objects.sort(key=lambda x: x.name, reverse=False)
            self.create_resource_attr_in_single_file(False)
            #self.create_resource_attr_in_single_file(True)
            #self.create_resource_pybind()
        else:
            # TODO, this is broken at the moment
            # PyBinding won't work properly, as the headers needed to added in the PyBind file
            for json_obj in self.json_objects:
                json_obj.generate()
            self.create_resource_pybind()


class ResourceAttr:
    """
    Process Resource Attributes and creates list of string constants
    Props is a list of dicts
    """
    props = {}

    def __init__(self, template_dir, out_dir):
        self.props = {}
        self.template_dir = template_dir
        self.out_dir = out_dir

    def add_props(self, props):
        temp = collections.OrderedDict(sorted(props.items(),
                        key=lambda item: item[0]))
        t = {**self.props, **temp}
        self.props = collections.OrderedDict(sorted(t.items(),
                        key=lambda item: item[0]))

    def process(self):
        env = Environment(loader=FileSystemLoader(self.template_dir),
                          trim_blocks=True,
                          lstrip_blocks=False)
        env.filters['string_constant'] = string_constant

        attr_list = []
        for prop, prop_data in self.props.items():
            attr_list.append(prop)
        template_env = env.get_template("IoTProperties.h.jinja2")
        text = template_env.render(schema_data=attr_list)
        file_name = "IoTProperties.h"
        write_file(file_name, text, self.out_dir)

        template_env = env.get_template("IoTProperties.mm.jinja2")
        text = template_env.render(schema_data=attr_list)
        file_name = "IoTProperties.mm"
        write_file(file_name, text, self.out_dir)
