#
# Asterisk -- An open source telephony toolkit.
#
# Copyright (C) 2013, Digium, Inc.
#
# David M. Lee, II <dlee@digium.com>
#
# See http://www.asterisk.org for more information about
# the Asterisk project. Please do not directly contact
# any of the maintainers of this project for assistance;
# the project provides a web site, mailing lists and IRC
# channels for your use.
#
# This program is free software, distributed under the terms of
# the GNU General Public License Version 2. See the LICENSE file
# at the top of the source tree.
#

"""Implementation of SwaggerPostProcessor which adds fields needed to generate
Asterisk RESTful HTTP binding code.
"""

import os
import re

from swagger_model import Stringify, SwaggerError, SwaggerPostProcessor

try:
    from collections import OrderedDict
except ImportError:
    from odict import OrderedDict


def simple_name(name):
    """Removes the {markers} from a path segement.

    @param name: Swagger path segement, with {pathVar} markers.
    """
    if name.startswith('{') and name.endswith('}'):
        return name[1:-1]
    return name


def wikify(str):
    """Escapes a string for the wiki.

    @param str: String to escape
    """
    # Replace all line breaks with line feeds
    str = re.sub(r'<br\s*/?>', '\n', str)
    return re.sub(r'([{}\[\]])', r'\\\1', str)


def snakify(name):
    """Helper to take a camelCase or dash-seperated name and make it
    snake_case.
    """
    r = ''
    prior_lower = False
    for c in name:
        if c.isupper() and prior_lower:
            r += "_"
        if c == '-':
            c = '_'
        prior_lower = c.islower()
        r += c.lower()
    return r


class PathSegment(Stringify):
    """Tree representation of a Swagger API declaration.
    """
    def __init__(self, name, parent):
        """Ctor.

        @param name: Name of this path segment. May have {pathVar} markers.
        @param parent: Parent PathSegment.
        """
        #: Segment name, with {pathVar} markers removed
        self.name = simple_name(name)
        #: True if segment is a {pathVar}, else None.
        self.is_wildcard = None
        #: Underscore seperated name all ancestor segments
        self.full_name = None
        #: Dictionary of child PathSegements
        self.__children = OrderedDict()
        #: List of operations on this segement
        self.operations = []

        if self.name != name:
            self.is_wildcard = True

        if not self.name:
            assert(not parent)
            self.full_name = ''
        if not parent or not parent.name:
            self.full_name = name
        else:
            self.full_name = "%s_%s" % (parent.full_name, self.name)

    def get_child(self, path):
        """Walks descendants to get path, creating it if necessary.

        @param path: List of path names.
        @return: PageSegment corresponding to path.
        """
        assert simple_name(path[0]) == self.name
        if (len(path) == 1):
            return self
        child = self.__children.get(path[1])
        if not child:
            child = PathSegment(path[1], self)
            self.__children[path[1]] = child
        return child.get_child(path[1:])

    def children(self):
        """Gets list of children.
        """
        return self.__children.values()

    def num_children(self):
        """Gets count of children.
        """
        return len(self.__children)


class AsteriskProcessor(SwaggerPostProcessor):
    """A SwaggerPostProcessor which adds fields needed to generate Asterisk
    RESTful HTTP binding code.
    """

    #: How Swagger types map to C.
    type_mapping = {
        'string': 'const char *',
        'boolean': 'int',
        'number': 'int',
        'int': 'int',
        'long': 'long',
        'double': 'double',
        'float': 'float',
    }

    #: String conversion functions for string to C type.
    convert_mapping = {
        'string': '',
        'int': 'atoi',
        'long': 'atol',
        'double': 'atof',
        'boolean': 'ast_true',
    }

    #: JSON conversion functions
    json_convert_mapping = {
        'string': 'ast_json_string_get',
        'int': 'ast_json_integer_get',
        'long': 'ast_json_integer_get',
        'double': 'ast_json_real_get',
        'boolean': 'ast_json_is_true',
    }

    def __init__(self, wiki_prefix):
        self.wiki_prefix = wiki_prefix

    def process_resource_api(self, resource_api, context):
        resource_api.wiki_prefix = self.wiki_prefix
        # Derive a resource name from the API declaration's filename
        resource_api.name = re.sub('\..*', '',
                                   os.path.basename(resource_api.path))
        # Now in all caps, for include guard
        resource_api.name_caps = resource_api.name.upper()
        resource_api.name_title = resource_api.name.capitalize()
        resource_api.c_name = snakify(resource_api.name)
        # Construct the PathSegement tree for the API.
        if resource_api.api_declaration:
            resource_api.root_path = PathSegment('', None)
            for api in resource_api.api_declaration.apis:
                segment = resource_api.root_path.get_child(api.path.split('/'))
                for operation in api.operations:
                    segment.operations.append(operation)
                api.full_name = segment.full_name

            # Since every API path should start with /[resource], root should
            # have exactly one child.
            if resource_api.root_path.num_children() != 1:
                raise SwaggerError(
                    "Should not mix resources in one API declaration", context)
            # root_path isn't needed any more
            resource_api.root_path = list(resource_api.root_path.children())[0]
            if resource_api.name != resource_api.root_path.name:
                raise SwaggerError(
                    "API declaration name should match", context)
            resource_api.root_full_name = resource_api.root_path.full_name

    def process_api(self, api, context):
        api.wiki_path = wikify(api.path)

    def process_operation(self, operation, context):
        # Nicknames are camelCase, Asterisk coding is snake case
        operation.c_nickname = snakify(operation.nickname)
        operation.c_http_method = 'AST_HTTP_' + operation.http_method
        if not operation.summary.endswith("."):
            raise SwaggerError("Summary should end with .", context)
        operation.wiki_summary = wikify(operation.summary or "")
        operation.wiki_notes = wikify(operation.notes or "")
        for error_response in operation.error_responses:
            error_response.wiki_reason = wikify(error_response.reason or "")
        operation.parse_body = (operation.body_parameter or operation.has_query_parameters) and True

    def process_parameter(self, parameter, context):
        if parameter.param_type == 'body':
            parameter.is_body_parameter = True;
            parameter.c_data_type = 'struct ast_json *'
        else:
            parameter.is_body_parameter = False;
            if not parameter.data_type in self.type_mapping:
                raise SwaggerError(
                    "Invalid parameter type %s" % parameter.data_type, context)
            # Type conversions
            parameter.c_data_type = self.type_mapping[parameter.data_type]
            parameter.c_convert = self.convert_mapping[parameter.data_type]
            parameter.json_convert = self.json_convert_mapping[parameter.data_type]

        # Parameter names are camelcase, Asterisk convention is snake case
        parameter.c_name = snakify(parameter.name)
        # You shouldn't put a space between 'char *' and the variable
        if parameter.c_data_type.endswith('*'):
            parameter.c_space = ''
        else:
            parameter.c_space = ' '
        parameter.wiki_description = wikify(parameter.description)
        if parameter.allowable_values:
            parameter.wiki_allowable_values = parameter.allowable_values.to_wiki()
        else:
            parameter.wiki_allowable_values = None

    def process_model(self, model, context):
        model.description_dox = model.description.replace('\n', '\n * ')
        model.description_dox = re.sub(' *\n', '\n', model.description_dox)
        model.wiki_description = wikify(model.description)
        model.c_id = snakify(model.id)
        return model

    def process_property(self, prop, context):
        if "-" in prop.name:
            raise SwaggerError("Property names cannot have dashes", context)
        if prop.name != prop.name.lower():
            raise SwaggerError("Property name should be all lowercase",
                               context)
        prop.wiki_description = wikify(prop.description)

    def process_type(self, swagger_type, context):
        swagger_type.c_name = snakify(swagger_type.name)
        swagger_type.c_singular_name = snakify(swagger_type.singular_name)
        swagger_type.wiki_name = wikify(swagger_type.name)
