from __future__ import absolute_import, division, print_function, unicode_literals

import json

from stone.ir import (
    is_struct_type,
    is_union_type,
    is_void_type,
)
from stone.backends.swift import (
    base,
    fmt_serial_type,
    SwiftBaseBackend,
    undocumented,
)
from stone.backends.swift_helpers import (
    check_route_name_conflict,
    fmt_class,
    fmt_func,
    fmt_var,
    fmt_type,
)

_MYPY = False
if _MYPY:
    import typing  # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression

# Hack to get around some of Python 2's standard library modules that
# accept ascii-encodable unicode literals in lieu of strs, but where
# actually passing such literals results in errors with mypy --py2. See
# <https://github.com/python/typeshed/issues/756> and
# <https://github.com/python/mypy/issues/2536>.
import importlib
argparse = importlib.import_module(str('argparse'))  # type: typing.Any


_cmdline_parser = argparse.ArgumentParser(
    prog='swift-client-backend',
    description=(
        'Generates a Swift class with an object for each namespace, and in each '
        'namespace object, a method for each route. This class assumes that the '
        'swift_types backend was used with the same output directory.'),
)
_cmdline_parser.add_argument(
    '-m',
    '--module-name',
    required=True,
    type=str,
    help=('The name of the Swift module to generate. Please exclude the .swift '
          'file extension.'),
)
_cmdline_parser.add_argument(
    '-c',
    '--class-name',
    required=True,
    type=str,
    help=('The name of the Swift class that contains an object for each namespace, '
          'and in each namespace object, a method for each route.')
)
_cmdline_parser.add_argument(
    '-t',
    '--transport-client-name',
    required=True,
    type=str,
    help='The name of the Swift class that manages network API calls.',
)
_cmdline_parser.add_argument(
    '-y',
    '--client-args',
    required=True,
    type=str,
    help='The client-side route arguments to append to each route by style type.',
)
_cmdline_parser.add_argument(
    '-z',
    '--style-to-request',
    required=True,
    type=str,
    help='The dict that maps a style type to a Swift request object name.',
)


class SwiftBackend(SwiftBaseBackend):
    """
    Generates Swift client base that implements route interfaces.

    Examples:

    ```
    open class ExampleClientBase {
        /// Routes within the namespace1 namespace. See Namespace1 for details.
        open var namespace1: Namespace1!
        /// Routes within the namespace2 namespace. See Namespace2 for details.
        open var namespace2: Namespace2!

        public init(client: ExampleTransportClient) {
            self.namespace1 = Namespace1(client: client)
            self.namespace2 = Namespace2(client: client)
        }
    }
    ```

    Here, `ExampleTransportClient` would contain the implementation of a handwritten,
    project-specific networking client. Additionally, the `Namespace1` object would
    have as its methods all routes in the `Namespace1` namespace. A hypothetical 'copy'
    enpoding might be implemented like:

    ```
    open func copy(fromPath fromPath: String, toPath: String) ->
                     ExampleRequestType<Namespace1.CopySerializer, Namespace1.CopyErrorSerializer> {
        let route = Namespace1.copy
        let serverArgs = Namespace1.CopyArg(fromPath: fromPath, toPath: toPath)
        return client.request(route, serverArgs: serverArgs)
    }
    ```

    Here, ExampleRequestType is a project-specific request type, parameterized by response and
    error serializers.
    """

    cmdline_parser = _cmdline_parser

    def generate(self, api):
        for namespace in api.namespaces.values():
            ns_class = fmt_class(namespace.name)
            if namespace.routes:
                with self.output_to_relative_path('{}Routes.swift'.format(ns_class)):
                    self._generate_routes(namespace)

        with self.output_to_relative_path('{}.swift'.format(self.args.module_name)):
            self._generate_client(api)

    def _generate_client(self, api):
        self.emit_raw(base)
        self.emit('import Alamofire')
        self.emit()

        with self.block('open class {}'.format(self.args.class_name)):
            namespace_fields = []
            for namespace in api.namespaces.values():
                if namespace.routes:
                    namespace_fields.append((namespace.name,
                                            fmt_class(namespace.name)))
            for var, typ in namespace_fields:
                self.emit('/// Routes within the {} namespace. '
                          'See {}Routes for details.'.format(var, typ))
                self.emit('open var {}: {}Routes!'.format(var, typ))
            self.emit()

            with self.function_block('public init', args=self._func_args(
                    [('client', '{}'.format(self.args.transport_client_name))])):
                for var, typ in namespace_fields:
                    self.emit('self.{} = {}Routes(client: client)'.format(var, typ))

    def _generate_routes(self, namespace):
        check_route_name_conflict(namespace)

        ns_class = fmt_class(namespace.name)
        self.emit_raw(base)
        self.emit('/// Routes for the {} namespace'.format(namespace.name))

        with self.block('open class {}Routes'.format(ns_class)):
            self.emit('public let client: {}'.format(self.args.transport_client_name))
            args = [('client', '{}'.format(self.args.transport_client_name))]

            with self.function_block('init', self._func_args(args)):
                self.emit('self.client = client')

            self.emit()

            for route in namespace.routes:
                self._generate_route(namespace, route)

    def _get_route_args(self, namespace, route):
        data_type = route.arg_data_type
        arg_type = fmt_type(data_type)
        if is_struct_type(data_type):
            arg_list = self._struct_init_args(data_type, namespace=namespace)

            doc_list = [(fmt_var(f.name), self.process_doc(f.doc, self._docf)
                if f.doc else undocumented) for f in data_type.fields if f.doc]
        elif is_union_type(data_type):
            arg_list = [(fmt_var(data_type.name), '{}.{}'.format(
                fmt_class(namespace.name), fmt_class(data_type.name)))]
            doc_list = [(fmt_var(data_type.name),
                self.process_doc(data_type.doc, self._docf)
                if data_type.doc else 'The {} union'.format(fmt_class(data_type.name)))]
        else:
            arg_list = [] if is_void_type(data_type) else [('request', arg_type)]
            doc_list = []
        return arg_list, doc_list

    def _emit_route(self, namespace, route, req_obj_name, extra_args=None, extra_docs=None):
        arg_list, doc_list = self._get_route_args(namespace, route)
        extra_args = extra_args or []
        extra_docs = extra_docs or []

        arg_type = fmt_type(route.arg_data_type)
        func_name = fmt_func(route.name, route.version)

        if route.doc:
            route_doc = self.process_doc(route.doc, self._docf)
        else:
            route_doc = 'The {} route'.format(func_name)
        self.emit_wrapped_text(route_doc, prefix='/// ', width=120)
        self.emit('///')

        for name, doc in doc_list + extra_docs:
            param_doc = '- parameter {}: {}'.format(name, doc if doc is not None else undocumented)
            self.emit_wrapped_text(param_doc, prefix='/// ', width=120)
        self.emit('///')
        output = (' - returns: Through the response callback, the caller will ' +
            'receive a `{}` object on success or a `{}` object on failure.')
        output = output.format(fmt_type(route.result_data_type),
                               fmt_type(route.error_data_type))
        self.emit_wrapped_text(output, prefix='/// ', width=120)

        func_args = [
            ('route', '{}.{}'.format(fmt_class(namespace.name), func_name)),
        ]
        client_args = []
        return_args = [('route', 'route')]

        for name, value, typ in extra_args:
            arg_list.append((name, typ))
            func_args.append((name, value))
            client_args.append((name, value))

        rtype = fmt_serial_type(route.result_data_type)
        etype = fmt_serial_type(route.error_data_type)

        self._maybe_generate_deprecation_warning(route)

        with self.function_block('@discardableResult open func {}'.format(func_name),
                args=self._func_args(arg_list, force_first=False),
                return_type='{}<{}, {}>'.format(req_obj_name, rtype, etype)):
            self.emit('let route = {}.{}'.format(fmt_class(namespace.name), func_name))
            if is_struct_type(route.arg_data_type):
                args = [(name, name) for name, _ in self._struct_init_args(route.arg_data_type)]
                func_args += [('serverArgs', '{}({})'.format(arg_type, self._func_args(args)))]
                self.emit('let serverArgs = {}({})'.format(arg_type, self._func_args(args)))
            elif is_union_type(route.arg_data_type):
                self.emit('let serverArgs = {}'.format(fmt_var(route.arg_data_type.name)))

            if not is_void_type(route.arg_data_type):
                return_args += [('serverArgs', 'serverArgs')]

            return_args += client_args

            txt = 'return client.request({})'.format(
                self._func_args(return_args, not_init=True)
            )

            self.emit(txt)
        self.emit()

    def _maybe_generate_deprecation_warning(self, route):
        if route.deprecated:
            msg = '{} is deprecated.'.format(fmt_func(route.name, route.version))
            if route.deprecated.by:
                msg += ' Use {}.'.format(
                    fmt_func(route.deprecated.by.name, route.deprecated.by.version))
            self.emit('@available(*, unavailable, message:"{}")'.format(msg))

    def _generate_route(self, namespace, route):
        route_type = route.attrs.get('style')
        client_args = json.loads(self.args.client_args)
        style_to_request = json.loads(self.args.style_to_request)

        if route_type not in client_args.keys():
            self._emit_route(namespace, route, style_to_request[route_type])
        else:
            for args_data in client_args[route_type]:
                req_obj_key, type_data_list = tuple(args_data)
                req_obj_name = style_to_request[req_obj_key]

                extra_args = [tuple(type_data[:-1]) for type_data in type_data_list]
                extra_docs = [(type_data[0], type_data[-1]) for type_data in type_data_list]

                self._emit_route(namespace, route, req_obj_name, extra_args, extra_docs)
