"""Example backend that outputs a Stone file equivalent to the input file.

Current limitations:
- Whitespace is not reproduced exactly (this may be a feature)
- Order of definitions is lost
- Comments are lost
- Aliases are lost (they are expanded in-line)
- Docstrings are reformatted
"""

from stone.backend import CodeBackend
from stone.frontend.ast import AstTypeRef
from stone.ir import DataType
from stone.ir import List, String, Timestamp
from stone.ir import Struct, Union, Void
from stone.ir.data_types import _BoundedInteger, _BoundedFloat


class UnstoneBackend(CodeBackend):
    """Main class.

    The Stone CLI finds this class through introspection."""

    def generate(self, api):
        """Main code generator entry point."""
        # Create a file for each namespace.
        for namespace in api.namespaces.values():
            with self.output_to_relative_path('%s.stone' % namespace.name):
                # Output a namespace header.
                self.emit('namespace %s' % namespace.name)
                # Output all data type (struct and union) definitions.
                for data_type in namespace.linearize_data_types():
                    self.generate_data_type(data_type)
                # Output all route definitions.
                for route in namespace.routes:
                    self.generate_route(route)

    def generate_data_type(self, data_type):
        """Output a data type definition (a struct or union)."""
        if isinstance(data_type, Struct):
            # Output a struct definition.
            self.emit('')
            self.emit('struct %s' % data_type.name)
            with self.indent():
                if data_type.doc is not None:
                    self.emit(self.format_string(data_type.doc))
                for field in data_type.fields:
                    type_repr = self.format_data_type(field.data_type)
                    if not field.has_default:
                        self.emit('{} {}'.format(field.name, type_repr))
                    else:
                        self.emit('%s %s = %s' %
                            (field.name, type_repr, self.format_value(field.default)))
                    if field.doc is not None:
                        with self.indent():
                            self.emit(self.format_value(field.doc))
        elif isinstance(data_type, Union):
            # Output a union definition.
            self.emit('')
            self.emit('union %s' % data_type.name)
            with self.indent():
                if data_type.doc is not None:
                    self.emit(self.format_string(data_type.doc))
                for field in data_type.fields:
                    name = field.name
                    # Add a star for a catch-all field.
                    # (There are two ways to recognize these.)
                    if field.catch_all or field is data_type.catch_all_field:
                        name += '*'
                    if isinstance(field.data_type, Void):
                        self.emit('%s' % (name))
                    else:
                        type_repr = self.format_data_type(field.data_type)
                        self.emit('{} {}'.format(name, type_repr))
                    if field.doc is not None:
                        with self.indent():
                            self.emit(self.format_value(field.doc))
        else:
            # Don't know what this is.
            self.emit('')
            self.emit('# ??? %s' % repr(data_type))

    def generate_route(self, route):
        """Output a route definition."""
        self.emit('')
        self.emit('route {} ({}, {}, {})'.format(
            route.name,
            self.format_data_type(route.arg_data_type),
            self.format_data_type(route.result_data_type),
            self.format_data_type(route.error_data_type)
        ))
        # Output the docstring.
        with self.indent():
            if route.doc is not None:
                self.emit(self.format_string(route.doc))

    # Table describing data types with parameters.
    _data_type_map = [
        (_BoundedInteger, ['min_value', 'max_value']),
        (_BoundedFloat, ['min_value', 'max_value']),
        (List, ['data_type', 'min_items', 'max_items']),
        (String, ['min_length', 'max_length', 'pattern']),
        (Timestamp, ['format']),
    ]

    def format_data_type(self, data_type):
        """Helper function to format a data type.

        This returns the name if it's a struct or union, otherwise
        (i.e. for primitive types) it renders the name and the
        parameters.
        """
        s = data_type.name
        for type_class, key_list in self._data_type_map:
            if isinstance(data_type, type_class):
                args = []
                for key in key_list:
                    val = getattr(data_type, key)
                    if val is not None:
                        if isinstance(val, AstTypeRef):
                            sval = val.name
                        elif isinstance(val, DataType):
                            sval = self.format_data_type(val)
                        else:
                            sval = self.format_value(val)
                        args.append(key + '=' + sval)
                if args:
                    s += '(' + ', '.join(args) + ')'
                break
        if data_type.nullable:
            s += '?'
        return s

    def format_value(self, val):
        """Helper function to format a value."""
        if isinstance(val, str):
            return self.format_string(val)
        else:
            return str(val)

    def format_string(self, val):
        """Helper function to format a string."""
        return '"' + val.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\n\n') + '"'
