import logging
from pprint import pformat
import os
from .helper import *
from .jinjafilters import *
from .exceptions import *
from .config import *
from .schema import get_schema_from_method_data, get_method_body_params_data
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 strip_uri_convention(text):
    """Helper function to strip any unwanted strings in the URI"""
    temp = strip_left(text, '/')
    return strip_right(temp, 'ResURI')


def is_composite(swagger_data):
    """Checks if the links json element is present in the Swagger File
    :param swagger_data: Swagger data loaded from JSON file
    :return: True, if the links present. False otherwise.
    """

    for path, path_data in swagger_data['paths'].items():
        try:
            if "links" in path_data['get']['responses']['200']['x-subresources']:
                return True
        except KeyError:
            pass

        try:
            if "links" in path_data['get']['responses']['200']['x-example']:
                return True
        except KeyError:
            pass

    return False


def get_supported_crud(path_data):
    """
    Fetches the list of supported CRUD for each path
    :param path_data: Swagger Path data of an URI
    :return: list of supported CRUD
    """
    crud = list(path_data.keys())
    return sorted(crud)


def convert_list_to_string(ll):
    value = ""
    if ll:
        for l in ll:
            value += "\""
            value += l
            value += "\""
            value += ','
    return strip_right(value, ',')


def get_supported_ifaces(path_data):
    """
    Fetches the list of supported interfaces for each URI
    :param path_data: Swagger Path data of an URI
    :return: returns list of support interfaces by an URI
    """
    ifaces = []
    for method, method_data in path_data.items():
        params = method_data['parameters']
        for param in params:
            if 'in' in param and 'name' in param and 'enum' in param \
                    and 'query' in param['in'] and 'if' in param['name']:
                ifaces += param['enum']

    ifaces = sorted(set(ifaces))
    return convert_list_to_string(ifaces)


def get_supported_res_types(res_type):
    value = ""
    value += "\""
    value += res_type
    value += "\""
    return value


class CreateServerClientFiles:
    """Class to create Resource Servers and Resource Clients

    Creates
    1. ResourceServer.h
    2. ResourceServer.cpp
    3. ResourceClient.h
    4. ResourceClient.mm

    If the single_file attribute is False, then each Resource Server and Client is generated in a
    separate file
    """

    def __init__(self, env, filename_prefix, file_content):
        self.env = env
        self.single_file = swagger_config['single_file']
        self.out_dir = swagger_config['out_dir']
        self.filename_prefix = filename_prefix
        self.file_content = file_content

    def create(self):
        self.create_resource_client_header()
        self.create_resource_client_m()
        self.create_device_controller_header()
        self.create_device_controller_m()

    def create_resource_client_header(self):
        if self.single_file:
            template_h_env = self.env.get_template("IoTResourceClients.h.jinja2")
            header_text = template_h_env.render(swagger_objects=self.file_content)
            file_name = "IoTResourceClients.h"
        else:
            template_h_env = self.env.get_template("resourceclient.h.jinja2")
            header_text = template_h_env.render(swagger_data=self.file_content)
            file_name = self.filename_prefix.lower() + "_resource_client.h"
        write_file(file_name, header_text, self.out_dir)

    def create_resource_client_m(self):
        if self.single_file:
            template_cpp_env = self.env.get_template("IoTResourceClients.m.jinja2")
            cpp_text = template_cpp_env.render(swagger_objects=self.file_content)
            file_name = "IoTResourceClients.m"
        else:
            template_cpp_env = self.env.get_template("resourceclient.mm.jinja2")
            cpp_text = template_cpp_env.render(swagger_data=self.file_content)
            file_name = self.filename_prefix.lower() + "_resource_client.mm"
        write_file(file_name, cpp_text, self.out_dir)

    def create_device_controller_header(self):
        if self.single_file:
            template_h_env = self.env.get_template("IoTAllPlayController.h.jinja2")
            header_text = template_h_env.render(swagger_objects=self.file_content)
            file_name = "IoTAllPlayController.h"
        else:
            template_h_env = self.env.get_template("client_controller.h.jinja2")
            header_text = template_h_env.render(swagger_data=self.file_content)
            file_name = self.filename_prefix.lower() + "_client_controller.h"
        write_file(file_name, header_text, self.out_dir)

        if self.single_file:
            template_h_env = self.env.get_template("IoTSysController.h.jinja2")
            header_text = template_h_env.render(swagger_objects=self.file_content)
            file_name = "IoTSysController.h"
        else:
            template_h_env = self.env.get_template("client_controller.h.jinja2")
            header_text = template_h_env.render(swagger_data=self.file_content)
            file_name = self.filename_prefix.lower() + "_client_controller.h"
        write_file(file_name, header_text, self.out_dir)

    def create_device_controller_m(self):
        if self.single_file:
            template_cpp_env = self.env.get_template("IoTAllPlayController.m.jinja2")
            cpp_text = template_cpp_env.render(swagger_objects=self.file_content)
            file_name = "IoTAllPlayController.m"
        else:
            template_cpp_env = self.env.get_template("client_controller.mm.jinja2")
            cpp_text = template_cpp_env.render(swagger_data=self.file_content)
            file_name = self.filename_prefix.lower() + "_client_controller.mm"
        write_file(file_name, cpp_text, self.out_dir)

        if self.single_file:
            template_cpp_env = self.env.get_template("IoTSysController.m.jinja2")
            cpp_text = template_cpp_env.render(swagger_objects=self.file_content)
            file_name = "IoTSysController.m"
        else:
            template_cpp_env = self.env.get_template("client_controller.mm.jinja2")
            cpp_text = template_cpp_env.render(swagger_data=self.file_content)
            file_name = self.filename_prefix.lower() + "_client_controller.mm"
        write_file(file_name, cpp_text, self.out_dir)


class Resource:
    """Class representing each path in the swagger file"""

    # Resource information
    name = ""
    uri = ""
    prop = ""
    ifaces = ""
    type = ""
    crud = ""
    attr_classes = []
    attr_classes_crud = {}
    data = None

    def __str__(self):
        return str(self.__class__) + ": " + str(self.__dict__)

    def __init__(self):
        self.out_dir = swagger_config['out_dir']
        self.template_dir = swagger_config['template_dir']
        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['variable_syntax'] = variable_syntax

    def have_attrib(self):
        """
        Check, if the resource have any attributes, either in
        get response or post request
        :return: True/False
        """
        # Check, if get method supported or not, if yes some attribs available
        crud = get_supported_crud(self.data)
        if 'get' in crud:
            return True
        else:
            for method, method_data in self.data.items():
                params = method_data['parameters']
                for param in params:
                    if 'in' in param and 'body' in param['in']:
                        return True
            return False

    def have_attrib_classes(self):
        if len(self.attr_classes):
            return True
        else:
            return False

    def get_attr_classes(self):
        text = 'public '
        for attr_class in self.attr_classes:
            text += 'IoT' + class_name_syntax(attr_class) + 'Attr'
            text += ', '
        return text

    def get_attr_constructors(self):
        text = ''
        for attr_class in self.attr_classes:
            text += 'IoT' + class_name_syntax(attr_class) + 'Attr'
            text += '(), '
        return text

    def generate(self):
        """Create Resource Server and Client"""
        files = CreateServerClientFiles(self.env, self.name, self)
        files.create()


class Swagger:
    """
    Swagger parser: Parses the Swagger files to generate
    1. Resource Servers (headers and cpp)
    2. Resource Clients (headers and cpp)
    """
    # Generates, servers/clients in a file single, instead of generating each server/client 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

    # Swagger Data, in memory loaded from file. JSON deref done for $ref tags as well
    swagger_json = None
    swagger_reffed_json = None

    # Resource hierarchy for composite resources
    # { 'href': (parent_path, res_type, res_ifaces) }
    resource_hierarchy = {}

    # Resource type map, with key in lower case, loaded from restype.json file
    res_type_map = None

    # List of Resource class objects
    resources = []

    # Map of Path and the corresponding Attributes classes the Path depends on
    resource_attr_classes = {}
    resource_attr_classes_crud = {}

    def __init__(self):
        self.logger = logging.getLogger("Swagger")

        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.uri_prefix = swagger_config['uri_prefix']

        # Fetch the resource type mapping from restype.json
        for directory in self.schema_dir:
            self.fetch_res_type_map(directory)

            # Populates the parent uri information for each child resource, if any
            self.populate_resource_hierarchy(directory)

            # Populate Resource Attribute class list the Servers/Client need to derive from
            self.populate_resource_attr_list(directory)

            self.populate_resources(directory)
        self.logger.debug('*****************************************************')
        self.logger.debug('Resource Hierarchy')
        self.logger.debug(pformat(self.resource_hierarchy))
        self.logger.debug('*****************************************************')
        self.logger.debug('Resource Info')
        self.logger.debug(pformat(self.resources))

    def fetch_res_type_map(self, directory):
        """Loads the restype.json present in schema dir

        This mapping is used to derive the composite resource hierarchy
        :return: None
        """
        res_type_file = directory + "/restype.json"
        self.res_type_map = load_json_schema(res_type_file, False, directory)
        self.res_type_map = {k.lower(): v for k, v in self.res_type_map.items()}

    def populate_resource_hierarchy(self, directory):
        """Populates the resource hierarchy

        For each swagger file in the schema dir, checks if it a Composite resource. If
        Composite, updates resource hierarchy map with the following information
        Map of 'child uri i.e. href' as key and
        Tuple of parent URI, child resource type, child resource iface  as value
        Example:
            {'href': (parent_uri, res_type, res_iface)}
        """

        swagger_files = get_dir_list(directory, ".swagger.json")

        for swagger_file in swagger_files:
            swagger_reffed_json, swagger_json = load_json_schema(swagger_file, True, directory)

            # Populate resource hierarchy, for composites
            if is_composite(swagger_json):
                for path, path_data in swagger_json['paths'].items():
                    print ("Processing path:" + pformat(path))
                    links = []
                    try:
                        if "links" in path_data['get']['responses']['200']['x-subresources']:
                            links = path_data['get']['responses']['200']['x-subresources']['links']
                    except KeyError:
                        pass

                    try:
                        if "links" in path_data['get']['responses']['200']['x-example']:
                            links = path_data['get']['responses']['200']['x-example']['links']
                    except KeyError:
                        pass

                    for link in links:
                        print ("Processing link:" + pformat(link))
                        # (parent, resType, resIface)
                        self.resource_hierarchy[strip_uri_convention(link['href'])] = \
                            (strip_uri_convention(path), link['rt'], link['if'])

    def populate_resource_attr_list(self, directory):
        """Populate the Resource Attributes classes the Resource Servers/Clients derive from

        Resource attribute classes are created 'Schema.py". It derives this information by parsing the following
        1. Schema tags - in both Parameters or Responses section
            1.1 Schema tags could be a local ref - within the same file
            1.2 Schema tags could be a remote ref - to other file
            1.3 Schema tags without ref - in place object
        2. In place objects in parameters section
        3. Separate schema files - reused across different swaggers.

        Since schema files would be covered in item#1, for the purpose this function, looking for Attributes Classes
        in item#1 and item#2 is sufficient.
        :return:
        """
        swagger_files = get_dir_list(directory, ".swagger.json")
        for swagger_file in swagger_files:
            self.logger.debug("populate_resource_attr_list: Processing Swagger %s", swagger_file)
            swagger_reffed_json, swagger_json = load_json_schema(swagger_file, True, directory)

            for path, path_data in swagger_reffed_json['paths'].items():
                attr_list = []
                crud_attr = {}
                crud = get_supported_crud(path_data)
                for method in crud:
                    crud_attr_method = {}
                    # Get the schema data
                    
                    param_data = get_schema_from_method_data(path_data, method, 'parameters')
                    if not param_data:
                        param_data = get_method_body_params_data(path_data, method)
                    if param_data:
                        obj_name = strip_name_convention(param_data['name'])
                        attr_list.append(obj_name)
                        crud_attr_method["param"] = obj_name
                    schema_data = get_schema_from_method_data(path_data, method, 'responses')
                    if schema_data:
                        obj_name = strip_name_convention(schema_data['name'])
                        attr_list.append(obj_name)
                        crud_attr_method["response"] = obj_name

                    crud_attr[method] = crud_attr_method

                    # Get objects in params body section
                    param_data = get_method_body_params_data(path_data, method)
                    if param_data:
                        if 'name' in param_data:
                            obj_name = strip_name_convention(param_data['name'])
                            attr_list.append(obj_name)
                self.resource_attr_classes[strip_uri_convention(path)] = remove_list_duplicates(attr_list)
                self.resource_attr_classes_crud[strip_uri_convention(path)] = crud_attr
                logger.debug("Crud Attrs: " + pformat(crud_attr))


    def populate_resources(self, directory):
        """Creates Resource objects for each URI Path

        For each swagger file in the schema dir, iterates over the paths available and creates
        Resource Object with information required to create to create, ResourceServer and ResourceClient
        """
        swagger_files = get_dir_list(directory, ".swagger.json")
        for swagger_file in swagger_files:
            self.logger.debug("populate_resources: Processing Swagger %s", swagger_file)
            swagger_reffed_json, swagger_json = load_json_schema(swagger_file, True, directory)

            for path, path_data in swagger_reffed_json['paths'].items():
                path = strip_uri_convention(path)
                resource = Resource()
                resource.data = path_data
                resource.name = path
                resource.attr_classes = self.resource_attr_classes[path]
                resource.attr_classes_crud = self.resource_attr_classes_crud[path]
                resource.type = get_supported_res_types(self.res_type_map[path.lower()])
                resource.crud = get_supported_crud(path_data)
                uri_prefix = "" 
                if "allplay" in resource.type:
                    uri_prefix = "/qti/allplay"
                else:
                    uri_prefix = "/qti/iotsys"
                resource.uri = uri_prefix + self.create_res_uri(path)
                resource.prop = self.create_res_prop(resource.crud)
                resource.ifaces = get_supported_ifaces(path_data)
                self.resources.append(resource)

    def create_res_uri(self, uri):
        """For a child URI, prefix the parent URI"""
        temp = uri
        parent = ""
        while temp in self.resource_hierarchy:
                temp = self.resource_hierarchy[temp][0]
                parent = "/" + temp
        return parent + "/" + uri

    def create_res_prop(self, crud):
        """Populates resource properties, to control discoverability and observability"""
        if "get" in crud:
            return "OC_DISCOVERABLE | OC_OBSERVABLE"
        else:
            return 0

    def create_jinja_env(self):
        """Creates the Jinja2 Environment with necessary filters"""
        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['variable_syntax'] = variable_syntax

    def process(self):
        """Creates the Resource Servers and Resource Clients"""
        if self.single_file:
            self.create_jinja_env()
            self.resources.sort(key=lambda x: x.name, reverse=False)
            files = CreateServerClientFiles(self.env, "", self.resources)
            files.create()
        else:
            for resource in self.resources:
                resource.generate()
