from contextlib import contextmanager

from stone.backend import CodeBackend
from stone.backends.swift_helpers import (
    fmt_class,
    fmt_func,
    fmt_obj,
    fmt_type,
    fmt_var,
)
from stone.ir import (
    Boolean,
    Bytes,
    DataType,
    Float32,
    Float64,
    Int32,
    Int64,
    List,
    String,
    Timestamp,
    UInt32,
    UInt64,
    Void,
    is_list_type,
    is_timestamp_type,
    is_union_type,
    is_user_defined_type,
    unwrap_nullable,
)

_serial_type_table = {
    Boolean: 'BoolSerializer',
    Bytes: 'NSDataSerializer',
    Float32: 'FloatSerializer',
    Float64: 'DoubleSerializer',
    Int32: 'Int32Serializer',
    Int64: 'Int64Serializer',
    List: 'ArraySerializer',
    String: 'StringSerializer',
    Timestamp: 'NSDateSerializer',
    UInt32: 'UInt32Serializer',
    UInt64: 'UInt64Serializer',
    Void: 'VoidSerializer',
}


stone_warning = """\
///
/// Copyright (c) 2016 Dropbox, Inc. All rights reserved.
///
/// Auto-generated by Stone, do not modify.
///

"""

# This will be at the top of the generated file.
base = """\
{}\
import Foundation

""".format(stone_warning)


undocumented = '(no description)'


class SwiftBaseBackend(CodeBackend):
    """Wrapper class over Stone generator for Swift logic."""
    # pylint: disable=abstract-method

    @contextmanager
    def function_block(self, func, args, return_type=None):
        signature = '{}({})'.format(func, args)
        if return_type:
            signature += ' -> {}'.format(return_type)
        with self.block(signature):
            yield

    def _func_args(self, args_list, newlines=False, force_first=False, not_init=False):
        out = []
        first = True
        for k, v in args_list:
            # this is a temporary hack -- injected client-side args
            # do not have a separate field for default value. Right now,
            # default values are stored along with the type, e.g.
            # `Bool = True` is a type, hence this check.
            if first and force_first and '=' not in v:
                k = "{0} {0}".format(k)

            if first and v is not None and not_init:
                out.append('{}'.format(v))
            elif v is not None:
                out.append('{}: {}'.format(k, v))
            first = False
        sep = ', '
        if newlines:
            sep += '\n' + self.make_indent()
        return sep.join(out)

    @contextmanager
    def class_block(self, thing, protocols=None):
        protocols = protocols or []
        extensions = []

        if isinstance(thing, DataType):
            name = fmt_class(thing.name)
            if thing.parent_type:
                extensions.append(fmt_type(thing.parent_type))
        else:
            name = thing
        extensions.extend(protocols)

        extend_suffix = ': {}'.format(', '.join(extensions)) if extensions else ''

        with self.block('open class {}{}'.format(name, extend_suffix)):
            yield

    def _struct_init_args(self, data_type, namespace=None):  # pylint: disable=unused-argument
        args = []
        for field in data_type.all_fields:
            name = fmt_var(field.name)
            value = fmt_type(field.data_type)
            data_type, nullable = unwrap_nullable(field.data_type)

            if field.has_default:
                if is_union_type(data_type):
                    default = '.{}'.format(fmt_var(field.default.tag_name))
                else:
                    default = fmt_obj(field.default)
                value += ' = {}'.format(default)
            elif nullable:
                value += ' = nil'
            arg = (name, value)
            args.append(arg)
        return args

    def _docf(self, tag, val):
        if tag == 'route':
            if ':' in val:
                val, version = val.split(':', 1)
                version = int(version)
            else:
                version = 1
            return fmt_func(val, version)
        elif tag == 'field':
            if '.' in val:
                cls, field = val.split('.')
                return ('{} in {}'.format(fmt_var(field),
                        fmt_class(cls)))
            else:
                return fmt_var(val)
        elif tag in ('type', 'val', 'link'):
            return val
        else:
            return val

def fmt_serial_type(data_type):
    data_type, nullable = unwrap_nullable(data_type)

    if is_user_defined_type(data_type):
        result = '{}.{}Serializer'
        result = result.format(fmt_class(data_type.namespace.name),
            fmt_class(data_type.name))
    else:
        result = _serial_type_table.get(data_type.__class__, fmt_class(data_type.name))

        if is_list_type(data_type):
            result = result + '<{}>'.format(fmt_serial_type(data_type.data_type))

    return result if not nullable else 'NullableSerializer'


def fmt_serial_obj(data_type):
    data_type, nullable = unwrap_nullable(data_type)

    if is_user_defined_type(data_type):
        result = '{}.{}Serializer()'
        result = result.format(fmt_class(data_type.namespace.name),
            fmt_class(data_type.name))
    else:
        result = _serial_type_table.get(data_type.__class__, fmt_class(data_type.name))

        if is_list_type(data_type):
            result = result + '({})'.format(fmt_serial_obj(data_type.data_type))
        elif is_timestamp_type(data_type):
            result = result + '("{}")'.format(data_type.format)
        else:
            result = 'Serialization._{}'.format(result)

    return result if not nullable else 'NullableSerializer({})'.format(result)
