from __future__ import absolute_import, division, print_function, unicode_literals

from stone.backend import Backend  # noqa: F401 # pylint: disable=unused-import
from stone.ir.api import ApiNamespace  # noqa: F401 # pylint: disable=unused-import
from stone.ir import (
    Boolean,
    Bytes,
    Float32,
    Float64,
    Int32,
    Int64,
    List,
    String,
    Timestamp,
    UInt32,
    UInt64,
    Void,
    is_alias,
    is_list_type,
    is_struct_type,
    is_map_type,
    is_user_defined_type,
)
from stone.backends.helpers import (
    fmt_camel,
)

_base_type_table = {
    Boolean: 'boolean',
    Bytes: 'string',
    Float32: 'number',
    Float64: 'number',
    Int32: 'number',
    Int64: 'number',
    List: 'Array',
    String: 'string',
    UInt32: 'number',
    UInt64: 'number',
    Timestamp: 'Timestamp',
    Void: 'void',
}


def fmt_error_type(data_type, inside_namespace=None):
    """
    Converts the error type into a TypeScript type.
    inside_namespace should be set to the namespace that the reference
    occurs in, or None if this parameter is not relevant.
    """
    return 'Error<%s>' % fmt_type(data_type, inside_namespace)

def fmt_type_name(data_type, inside_namespace=None):
    """
    Produces a TypeScript type name for the given data type.
    inside_namespace should be set to the namespace that the reference
    occurs in, or None if this parameter is not relevant.
    """
    if is_user_defined_type(data_type) or is_alias(data_type):
        if data_type.namespace == inside_namespace:
            return data_type.name
        else:
            return '%s.%s' % (data_type.namespace.name, data_type.name)
    else:
        fmted_type = _base_type_table.get(data_type.__class__, 'Object')
        if is_list_type(data_type):
            fmted_type += '<' + fmt_type(data_type.data_type, inside_namespace) + '>'
        elif is_map_type(data_type):
            key_data_type = _base_type_table.get(data_type.key_data_type, 'string')
            value_data_type = fmt_type_name(data_type.value_data_type, inside_namespace)
            fmted_type = '{[key: %s]: %s}' % (key_data_type, value_data_type)
        return fmted_type

def fmt_polymorphic_type_reference(data_type, inside_namespace=None):
    """
    Produces a TypeScript type name for the meta-type that refers to the given
    struct, which belongs to an enumerated subtypes tree. This meta-type contains the
    .tag field that lets developers discriminate between subtypes.
    """
    # NOTE: These types are not properly namespaced, so there could be a conflict
    #       with other user-defined types. If this ever surfaces as a problem, we
    #       can defer emitting these types until the end, and emit them in a
    #       nested namespace (e.g., files.references.MetadataReference).
    return fmt_type_name(data_type, inside_namespace) + "Reference"

def fmt_type(data_type, inside_namespace=None):
    """
    Returns a TypeScript type annotation for a data type.
    May contain a union of enumerated subtypes.
    inside_namespace should be set to the namespace that the type reference
    occurs in, or None if this parameter is not relevant.
    """
    if is_struct_type(data_type) and data_type.has_enumerated_subtypes():
        possible_types = []
        possible_subtypes = data_type.get_all_subtypes_with_tags()
        for _, subtype in possible_subtypes:
            possible_types.append(fmt_polymorphic_type_reference(subtype, inside_namespace))
        if data_type.is_catch_all():
            possible_types.append(fmt_polymorphic_type_reference(data_type, inside_namespace))
        return fmt_union(possible_types)
    else:
        return fmt_type_name(data_type, inside_namespace)

def fmt_union(type_strings):
    """
    Returns a union type of the given types.
    """
    return '|'.join(type_strings) if len(type_strings) > 1 else type_strings[0]

def fmt_func(name, version):
    if version == 1:
        return fmt_camel(name)
    return fmt_camel(name) + 'V{}'.format(version)


def fmt_var(name):
    return fmt_camel(name)

def fmt_tag(cur_namespace, tag, val):
    """
    Processes a documentation reference.
    """
    if tag == 'type':
        fq_val = val
        if '.' not in val and cur_namespace is not None:
            fq_val = cur_namespace.name + '.' + fq_val
        return fq_val
    elif tag == 'route':
        if ':' in val:
            val, version = val.split(':', 1)
            version = int(version)
        else:
            version = 1
        return fmt_func(val, version) + "()"
    elif tag == 'link':
        anchor, link = val.rsplit(' ', 1)
        # There's no way to have links in TSDoc, so simply use JSDoc's formatting.
        # It's entirely possible some editors support this.
        return '[%s]{@link %s}' % (anchor, link)
    elif tag == 'val':
        # Value types seem to match JavaScript (true, false, null)
        return val
    elif tag == 'field':
        return val
    else:
        raise RuntimeError('Unknown doc ref tag %r' % tag)


def check_route_name_conflict(namespace):
    """
    Check name conflicts among generated route definitions. Raise a runtime exception when a
    conflict is encountered.
    """

    route_by_name = {}
    for route in namespace.routes:
        route_name = fmt_func(route.name, route.version)
        if route_name in route_by_name:
            other_route = route_by_name[route_name]
            raise RuntimeError(
                'There is a name conflict between {!r} and {!r}'.format(other_route, route))
        route_by_name[route_name] = route


def generate_imports_for_referenced_namespaces(backend, namespace, module_name_prefix):
    # type: (Backend, ApiNamespace, str) -> None

    imported_namespaces = namespace.get_imported_namespaces()
    if not imported_namespaces:
        return

    for ns in imported_namespaces:
        backend.emit(
            "import * as {namespace_name} from '{module_name_prefix}{namespace_name}';".format(
                module_name_prefix=module_name_prefix,
                namespace_name=ns.name
            )
        )
    backend.emit()
