import copy
import re
import common_with_cwrap
import yaml
from collections import OrderedDict, defaultdict

try:
    # use faster C loader if available
    from yaml import CLoader as Loader
except ImportError:
    from yaml import Loader


# matches `name`, `params` in `name(params)`
NAME_PARAM_REGEX = r'(\w+)\((.*)\)'


def argument_to_declaration(param, func=None):
    arg = {}
    arg['type'], name = param.split(' ')
    if (arg['type'].endswith('?')):
        arg['is_nullable'] = True
        arg['type'] = arg['type'].rstrip('?')
    if arg['type'] == 'Tensor':
        arg['type'] = 'THTensor*'
    elif arg['type'] == 'LongTensor':
        arg['type'] = 'THIndexTensor*'
    elif arg['type'] == 'Scalar':
        arg['type'] = 'accreal'
    elif arg['type'] == 'Generator*':
        arg['type'] = 'THGenerator*'

    match = re.match(r'IntArrayRef\[(\d+)\]', arg['type'])
    if match:
        arg['type'] = 'IntArrayRef'
        arg['size'] = int(match.group(1))

    if '=' in name:
        name, default = name.split('=')
        arg['optional'] = True
        arg['default'] = default
    arg['name'] = name

    return arg


def output_arguments(thnn_function):
    cname = thnn_function.name
    output_args = []

    # function_wrapper expects everything in a declaration to be in
    # the base type (i.e. THTensor*), but if we pull a THCUNN only
    # implementation, it will have THCTensor* as the arg type. So we
    # strip the THC here before returning
    def map_to_th_type(t):
        if t.startswith('THC'):
            t = t.replace('THC', 'TH')
        return t

    def is_output_arg(arg_name, func_name):
        if arg_name == 'output' and 'updateOutput' in cname:
            return True
        if name in {'gradInput', 'gradWeight', 'gradBias', 'gradGrid'}:
            return True
        if arg_name == 'indices' and 'updateOutput' in cname and 'Unpool' not in cname:
            # indices is an output argument in pooling and an input in unpooling
            return True
        return False

    for arg in thnn_function.arguments:
        name = arg.name
        if is_output_arg(name, cname):
            desc = {
                'type': map_to_th_type(arg.type),
                'name': camel_to_snake(name),
                'output': True,
            }
            if name.startswith('grad_'):
                desc['is_nullable'] = True
            output_args.append(desc)
    return output_args


def get_return(args):
    indices = [str(idx) for idx, arg in enumerate(args) if arg.get('output')]
    return 'argument {}'.format(','.join(indices))


ARGUMENT_MAPPINGS = {
    'k': 'kernel_size',
    'd': 'stride',
    'pad': 'padding',
    'p': 'padding',
    'o': 'output_size',
    'osize': 'output_size',
    'output': 'output_size',  # as a prefix e.g. outputW
    'isize': 'input_size',
    'dilation': 'dilation',
    'adj': 'output_padding',
    'a': 'output_padding',
}

DIMENSION_OFFSET = {
    'width': -1,
    'height': -2,
    'B': 0,
    'C': 1,
    'W': -1,
    'H': -2,
    'T': -3,
    'left': 0,
    'right': 1,
    'top': 2,
    'bottom': 3,
    'front': 4,
    'back': 5,
}

SUBSTITUTIONS = {
    'input': 'self',
    'weights': 'weight',
    'train': 'training',
    'val': 'value',
    'lambda': 'lambd',
    'negval': 'negative_slope',
}


def camel_to_snake(name):
    # from https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
    s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()


def get_thnn_args(thnn_function, params, inplace):
    params_by_name = {p['name']: p for p in params}

    def arg_expr(prefix, suffix):
        # e.g kW, kH
        name = ARGUMENT_MAPPINGS[prefix]
        if name not in params_by_name:
            raise RuntimeError('missing arg "{}" in {}'.format(name, thnn_function.name))
        param = params_by_name[name]
        if param['type'] == 'IntArrayRef' and 'size' in param:
            name = name + '_'
        # NB: We calculate the dimension based on the name of
        # the argument, not its positional order.  This means
        # that we may reorder arguments to get them in
        # the right place; e.g., if a THNN implementation
        # has arguments in the order kernelW, kernelH, we
        # will generate a caller that is kernel[1], kernel[0]
        # to order them in the correct way.
        index = DIMENSION_OFFSET[suffix]
        if index < 0:
            index += param['size']
        expr = '{}[{}]'.format(name, index)
        return {'type': 'EXPRESSION', 'name': expr}

    thnn_args = []
    for arg in thnn_function.arguments:
        name = arg.name
        if name == 'state':
            continue
        if inplace and name == 'output':
            name = 'self'
        aten_name = camel_to_snake(SUBSTITUTIONS.get(name, name))
        parts = aten_name.split('_')
        if aten_name in params_by_name:
            param = params_by_name[aten_name]
            if arg.is_optional:
                param['is_nullable'] = True
            thnn_args.append(copy.deepcopy(param))
        elif len(parts) == 2 and parts[0] in ARGUMENT_MAPPINGS and parts[1] in DIMENSION_OFFSET:
            # e.g. pad_left
            thnn_args.append(arg_expr(parts[0], parts[1]))
        elif name[-1] in DIMENSION_OFFSET and name[:-1] in ARGUMENT_MAPPINGS:
            # e.g kW, kH
            thnn_args.append(arg_expr(name[:-1], name[-1]))
        elif name == 'owidth' or name == 'oheight':
            thnn_args.append(arg_expr(name[0], name[1:]))
        elif name == 'scale':
            thnn_args.append({'type': 'EXPRESSION', 'name': '1'})
        elif name == 'inplace':
            thnn_args.append({'type': 'EXPRESSION', 'name': str(inplace).lower()})
        else:
            raise RuntimeError("{}: can't find binding for '{}'"
                               .format(thnn_function.name, name))
    return thnn_args


def remove_unused_args(args, thnn_args):
    """Returns the subset of args whose name appears in thnn_args"""
    def clean_name(name):
        name = name[:name.index('[')] if '[' in name else name
        if name.endswith('_'):
            name = name[:-1]
        return name
    uses = set([clean_name(arg['name']) for arg in thnn_args])
    uses.add('output_mask')
    args = [arg for arg in args if arg['name'] in uses]
    for arg in args:
        if 'default' in arg:
            del arg['default']
    return args


def unique_args(argslist):
    result = []
    seen = set()
    for args in argslist:
        for arg in args:
            if arg['name'] in seen:
                continue
            seen.add(arg['name'])
            result.append(arg)
    return result


def function_info(name, arguments, cimpls, buffers, backends, inplace, backend_types):
    """
    cimpls contains information use to call into THNN:
        cname: THNN function name
        arguments: arguments to functional call
        condition: [optional] guard around call
    """
    return {
        'mode': 'NN',
        'name': name,
        'cpu_bfloat16': True if backend_types is not None and 'CPU' in backend_types and
                'BFloat16' in backend_types['CPU'] else False,
        'cuda_bfloat16': True if backend_types is not None and 'CUDA' in backend_types and
                'BFloat16' in backend_types['CUDA'] else False,
        'backend_types': backend_types,
        'arguments': arguments,
        'return': 'argument 0' if inplace else get_return(arguments),
        'buffers': buffers,
        'backends': backends,
        'cimpls': cimpls,
        'variants': ['function'],
    }

def base_declaration(func, thnn_function, backends, backend_types, inplace=False):
    """Creates the NN function without any buffers in it's signature"""
    name, params = re.match(NAME_PARAM_REGEX, func['name']).groups()
    if inplace:
        name += '_'
    params = params.split(', ')
    arguments = [argument_to_declaration(a, func) for a in params]
    if not inplace:
        arguments += output_arguments(thnn_function)
    buffers = [argument_to_declaration('Tensor ' + buf)
               for buf in func.get('buffers', [])]

    return function_info(name, arguments, None, buffers, backends, inplace, backend_types)

def forward_declaration(base, thnn_function, backend_types, inplace=False):
    name = '{}_forward'.format(base['name'])
    if inplace:
        name += '_'

    arguments = [copy.deepcopy(arg) for arg in base['arguments']
                 if not arg.get('output')]

    arguments += output_arguments(thnn_function)
    for buffer in base['buffers']:
        buffer = copy.deepcopy(buffer)
        buffer['output'] = True
        arguments.append(buffer)

    thnn_args = get_thnn_args(thnn_function, arguments, inplace)
    arguments = remove_unused_args(arguments, thnn_args)
    cimpl = {'cname': thnn_function.name, 'arguments': thnn_args}

    return function_info(name, arguments, [cimpl], [], base['backends'], inplace, backend_types)

def backward_declaration(base, thnn_functions, backend_types):
    name = '{}_backward'.format(base['name'])

    arguments = []
    arguments.append({'type': 'THTensor*', 'name': 'grad_output'})
    arguments += [copy.deepcopy(arg) for arg in base['arguments']
                  if arg['name'] != 'inplace']
    arguments += base['buffers']

    # outputs from the forward may be inputs to the backwards
    for arg in arguments:
        if 'output' in arg:
            del arg['output']

    arguments += unique_args([output_arguments(f) for f in thnn_functions])

    def initialize_output_arg(arg):
        # the mask array<bool, N> specifies which return values to compute
        arg['mask'] = True
        arg['is_nullable'] = True

        # grad_weight and grad_bias need to be resized and zeroed
        if arg['name'] == 'grad_weight':
            arg['resize'] = 'weight'
            arg['zero'] = True
        if arg['name'] == 'grad_bias':
            dim = 1 if 'transpose' in name else 0
            arg['resize'] = [('weight', dim)]
            arg['zero'] = True

    is_batch_norm_backward = '_backward' in thnn_functions[0].name
    grad_params = []
    if len(thnn_functions) > 1 or is_batch_norm_backward:
        for arg in arguments:
            if arg.get('output', False):
                initialize_output_arg(arg)
            if 'Tensor' in arg['type'] and arg['name'].startswith('grad_') and \
                    'input' not in arg['name'] and 'output' not in arg['name']:
                grad_params.append(arg['name'])

    thnn_args = [get_thnn_args(f, arguments, False) for f in thnn_functions]
    arguments = remove_unused_args(arguments, unique_args(thnn_args))
    cimpls = []

    def get_condition(func):
        # only call into the THNN functions if the output args are not null
        if '_updateGradInput' in func.name:
            return 'grad_input_'
        if '_accGradParameters' in func.name:
            return ' || '.join(p + '_' for p in grad_params)
        return None

    for func, args in zip(thnn_functions, thnn_args):
        cimpl = {'cname': func.name, 'arguments': args}
        if len(thnn_functions) > 1:
            cimpl['condition'] = get_condition(func)
        cimpls.append(cimpl)

    output_args = [arg for arg in arguments if arg.get('output', False)]

    return function_info(name, arguments, cimpls, [], base['backends'], False, backend_types)


def parse_nn_yaml(filename):
    with open(filename, 'r') as f:
        return yaml.load(f, Loader=Loader)


include_only = '(updateOutput|updateGradInput|accGradParameters|backward)$'
exclude = 'LookupTable'


def run(paths):
    function_backends = defaultdict(list)
    header_functions = OrderedDict()

    headers = [p for p in paths if p.endswith('.h')]
    yamls = [p for p in paths if p.endswith('.yaml')]

    for path in headers:
        backend = 'CUDA' if re.search('THCU', path) else 'CPU'
        for func in common_with_cwrap.parse_header(path):
            if re.search(include_only, func.name) is None or re.search(exclude, func.name) is not None:
                continue
            function_backends[func.name].append(backend)
            if func.name not in header_functions:
                header_functions[func.name] = func

    bwd_suffixes = ['_updateGradInput', '_accGradParameters', '_backward']

    declarations = []
    for path in yamls:
        for func in parse_nn_yaml(path):
            cname = func['cname']
            backends = function_backends[cname + '_updateOutput']

            fwd_function = header_functions[cname + '_updateOutput']
            bwd_functions = []
            for suffix in bwd_suffixes:
                if cname + suffix in header_functions:
                    bwd_functions.append(header_functions[cname + suffix])

            default_scalar_types = ['Float', 'Double', 'Half']  # Half will be stripped for CPU backend
            forward_backend_types = {}
            backward_backend_types = {}
            for backend in backends:
                backend_props = func.get(backend, {})
                forward_backend_types[backend] = backend_props.get('forward_scalar_types', default_scalar_types)
                backward_backend_types[backend] = backend_props.get('backward_scalar_types', default_scalar_types)

            base = base_declaration(func, fwd_function, backends, None)
            declarations.append(forward_declaration(base, fwd_function, forward_backend_types))
            if bwd_functions:
                declarations.append(backward_declaration(base, bwd_functions, backward_backend_types))


            if func.get('has_inplace', False):
                declarations.append(base_declaration(func, fwd_function, backends, forward_backend_types, True))
                declarations.append(forward_declaration(base, fwd_function, forward_backend_types, True))

    return declarations
