# ***************************************************************
# Copyright (c) 2020 Jittor. Authors: 
#     Wenyang Zhou <576825820@qq.com>
#     Dun Liang <randonlang@gmail.com>. 
# All Rights Reserved.
# This file is subject to the terms and conditions defined in
# file 'LICENSE.txt', which is part of this source code package.
# ***************************************************************
import ast, astunparse
import numpy as np

pjmap = {
    # ***************************************************************
    # Module
    # ***************************************************************
    'Conv2d': {
        'pytorch': {
            'args': "in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros'"
        },
        'jittor': {
            'module': 'nn',
            'name': 'Conv',
            'args': 'in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True'
        },
        'links': {},
        'extras': {},
    },
    'ConvTranspose2d': {
        'pytorch': {
            'args': "in_channels, out_channels, kernel_size, stride=1, padding=0, output_padding=0, groups=1, bias=True, dilation=1, padding_mode='zeros'"
        },
        'jittor': {
            'module': 'nn',
            'name': 'ConvTranspose',
            'args': 'in_channels, out_channels, kernel_size, stride=1, padding=0, output_padding=0, groups=1, bias=True, dilation=1'
        },
        'links': {},
        'extras': {},
    },
    'MaxPool2d': {
        'pytorch': {
            'args': 'kernel_size, stride=None, padding=0, dilation=1, return_indices=False', 
        },
        'jittor': {
            'module': 'nn',
            'name': 'Pool',
            'args': 'kernel_size, stride=None, padding=0, dilation=None, return_indices=None, ceil_mode=False, op="maximum"'
        },
        'links': {},
        'extras': {
            "op": "'maximum'",
        },
    },
    'AvgPool2d': {
        'pytorch': {
            'args': 'kernel_size, stride=None, padding=0, dilation=1, return_indices=False', 
        },
        'jittor': {
            'module': 'nn',
            'name': 'Pool',
            'args': 'kernel_size, stride=None, padding=0, dilation=None, return_indices=None, ceil_mode=False, op="maximum"'
        },
        'links': {},
        'extras': {
            "op": "'mean'",
        },
    },
    'ReLU': {
        'pytorch': {
            'args': 'inplace=False', 
        },
        'jittor': {
            'module': 'nn',
            'name': 'ReLU',
            'args': ''
        },
        'links': {},
        'extras': {},
        'delete': ['inplace'],
    },
    'relu': {
        'pytorch': {
            'args': 'input', 
        },
        'jittor': {
            'module': 'nn',
            'name': 'relu',
            'args': 'x'
        },
        'links': {'input': 'x'},
        'extras': {},
        'delete': [],
    },
    'binary_cross_entropy_with_logits': {
        'pytorch': {
            'args': 'input, target, weight, size_average=True', 
        },
        'jittor': {
            'module': 'nn',
            'name': 'binary_cross_entropy_with_logits',
            'args': 'input, target, weight, size_average=True'
        },
        'links': {},
        'extras': {},
        'delete': [],
    },
    'ReLU6': {
        'pytorch': {
            'args': 'inplace=False', 
        },
        'jittor': {
            'module': 'nn',
            'name': 'ReLU6',
            'args': ''
        },
        'links': {},
        'extras': {},
        'delete': ['inplace'],
    },
    'PReLU': {
        'pytorch': {
            'args': 'num_parameters=1, init=0.25', 
        },
        'jittor': {
            'module': 'nn',
            'name': 'PReLU',
            'args': 'num_parameters=1, init_=0.25'
        },
        'links': {'init': 'init_'},
        'extras': {},
    },
    'LeakyReLU': {
        'pytorch': {
            'args': 'negative_slope=0.01, inplace=False', 
        },
        'jittor': {
            'module': 'nn',
            'name': 'LeakyReLU',
            'args': 'scale=0.01'
        },
        'links': {'negative_slope': 'scale'},
        'extras': {},
        'delete': ['inplace'],
    },
    'BatchNorm2d': {
        'pytorch': {
            'args': 'num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True', 
        },
        'jittor': {
            'module': 'nn',
            'name': 'BatchNorm',
            'args': 'num_features, eps=1e-5, momentum=0.1, affine=None, is_train=True'
        },
        'links': {},
        'extras': {},
    },
    'BatchNorm1d': {
        'pytorch': {
            'args': "num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True"
        },
        'jittor': {
            'module': 'nn',
            'name': 'BatchNorm1d',
            'args': 'num_features, eps=1e-5, momentum=0.1, affine=None, is_train=True, sync=True',
        },
        'links': {},
        'extras': {'affine': 'None'},
        'delete': ['track_running_stats'],
    },
    'GroupNorm': {
        'pytorch': {
            'args': "num_groups, num_channels, eps=1e-05, momentum=0.1, affine=True"
        },
        'jittor': {
            'module': 'nn',
            'name': 'GroupNorm',
            'args': 'num_groups, num_channels, eps=1e-05, affine=None, is_train=True',
        },
        'links': {},
        'extras': {'affine': 'None'},
    },
    'Dropout2d': {
        'pytorch': {
            'args': 'p=0.5, inplace=False', 
        },
        'jittor': {
            'module': 'nn',
            'name': 'Dropout',
            'args': 'p=0.5, is_train=False'
        },
        'links': {},
        'extras': {},
        'delete': ['inplace'],
    },
    'Upsample': {
        'pytorch': {
            'args': "size=None, scale_factor=None, mode='nearest', align_corners=None", 
        },
        'jittor': {
            'module': 'nn',
            'name': 'Upsample',
            'args': "scale_factor=None, mode='nearest'"
        },
        'links': {},
        'extras': {},
    },
    'kaiming_normal_': {
        'pytorch': {
            'args': "tensor, a=0, mode='fan_in', nonlinearity='leaky_relu'", 
        },
        'jittor': {
            'module': 'init',
            'name': 'relu_invariant_gauss_',
            'args': 'var, mode="fan_in"'
        },
        'links': {'tensor': 'var'},
        'extras': {},
    },
    'constant_': {
        'pytorch': {
            'args': "tensor, val", 
        },
        'jittor': {
            'module': 'init',
            'name': 'constant_',
            'args': 'var, value=0.0'
        },
        'links': {'tensor': 'var', 'val': 'value'},
        'extras': {},
    },
    'normal_': {
        'pytorch': {
            'args': "tensor, mean=0.0, std=1.0", 
        },
        'jittor': {
            'module': 'init',
            'name': 'gauss_',
            'args': 'var, mean=0.0, std=1.0'
        },
        'links': {'tensor': 'var'},
        'extras': {},
    },
    'uniform_': {
        'pytorch': {
            'args': "tensor, a=0.0, b=1.0", 
        },
        'jittor': {
            'module': 'init',
            'name': 'uniform_',
            'args': 'var, low, high'
        },
        'links': {'tensor': 'var', 'a': 'low', 'b': 'high'},
        'extras': {},
    },
    'cat': {
        'pytorch': {
            'args': "tensors, dim=0, out=None", 
        },
        'jittor': {
            'module': 'jt.contrib',
            'name': 'concat',
            'args': 'vars, dim=0'
        },
        'links': {'tensors': 'vars'},
        'extras': {},
    },
    # ***************************************************************
    # Convert format for function which can be writen as either torch.Tensor.xxx(...) or torch.xxx(torch.Tensor, ...)
    #       Example: x.reshape([2,3]) and torch.reshape(x, [2,3])
    # ***************************************************************
    'flatten': {
        'pytorch': {
            'prefix': ['torch'],
            'args_prefix': 'input, start_dim=0, end_dim=-1',
            'args': 'start_dim=0, end_dim=-1',
        },
        'jittor': {
            'prefix': 'jt',
            'module': '',
            'name': 'flatten',
            'args_prefix': 'input, start_dim=0, end_dim=-1',
            'args': 'start_dim=0, end_dim=-1'
        },
        'links': {},
        'extras': {},
    },
    'reshape': {
        'pytorch': {
            'prefix': ['torch'],
            'args_prefix': 'input, shape',
            'args': 'shape',
        },
        'jittor': {
            'prefix': 'jt',
            'module': '',
            'name': 'reshape',
            'args_prefix': 'input, shape',
            'args': 'shape'
        },
        'links': {},
        'extras': {},
    },
    'clamp': {
        'pytorch': {
            'prefix': ['torch'],
            'args_prefix': 'input, min, max, out=None',
            'args': 'min, max, out=None',
        },
        'jittor': {
            'prefix': 'jt',
            'module': '',
            'name': 'clamp',
            'args_prefix': 'x, min_v, max_v',
            'args': 'min_v, max_v'
        },
        'links': {'min': 'min_v', 'max': 'max_v'},
        'extras': {},
        'delete': ['out'],
    },
    'permute': {
        'pytorch': {
            'prefix': [],
            'args_prefix': '',
            'args': '*dim',
        },
        'jittor': {
            'prefix': '',
            'module': '',
            'name': 'permute',
            'args_prefix': '',
            'args': '*dim'
        },
        'links': {},
        'extras': {},
    },
    'view': {
        'pytorch': {
            'prefix': [],
            'args_prefix': '',
            'args': '*shape',
        },
        'jittor': {
            'prefix': '',
            'module': '',
            'name': 'view',
            'args_prefix': '',
            'args': '*shape'
        },
        'links': {},
        'extras': {},
    }
}


def pjmap_append(pytorch_func_name, pytorch_args, jittor_func_module, jittor_func_name, jittor_args, extras=None, links=None, delete=None):
    ''' adding map to pjmap for converting new function, example: convert AvgPool2d to Pool
    args:
        * `pytorch_func_name`: Pytorch function name
        * `pytorch_args`: Pytorch parameter list
        * `jittor_func_module`: to which module the Jittor function belongs
        * `jittor_func_name`: Jittor function name
        * `jittor_args`: Jittor parameter list
        * `extras`: parameter assignment
        * `links`: connection parameters
        * `delete`: delete parameters

    example:
        from jittor.utils.pytorch_converter import pjmap_append
        pjmap_append(pytorch_func_name='AvgPool2d', 
                    pytorch_args='kernel_size, stride=None, padding=0, dilation=1, return_indices=False',
                    jittor_func_module='nn',
                    jittor_func_name='Pool',
                    jittor_args='kernel_size, stride=None, padding=0, dilation=None, return_indices=None, ceil_mode=False, op="maximum"',
                    extras={"op": "'mean'"}) 
    '''
    if links == None: links = {}
    if extras == None: extras = {}
    if delete == None: delete = []
    assert isinstance(links, dict)
    assert isinstance(extras, dict)
    assert isinstance(delete, list)
    pjmap[pytorch_func_name] = {
        'pytorch': {
            'args': pytorch_args,
        },
        'jittor': {
            'module': jittor_func_module,
            'name': jittor_func_name,
            'args': jittor_args,
        },
        'links': links,
        'extras': extras,
        'delete': delete,
    }

unsupport_ops = [
    # ***************************************************************
    # torch.nn
    # ***************************************************************
    'Parameter', 'ModuleDict', 'ParameterList', 'ParameterDict', 
    'Conv1d', 'Conv3d', 'ConvTranspose1d', 'ConvTranspose3d', 'Unfold', 'Fold', 
    'MaxPool1d', 'MaxPool3d', 'MaxUnpool1d', 'MaxUnpool2d', 'MaxUnpool3d', 'AvgPool1d', 
    'AvgPool3d', 'FractionalMaxPool2d', 'LPPool1d', 'LPPool2d', 'AdaptiveMaxPool1d', 
    'AdaptiveMaxPool2d', 'AdaptiveMaxPool3d', 'AdaptiveAvgPool1d', 'AdaptiveAvgPool3d', 
    'ReflectionPad1d', 'ReplicationPad1d', 'ReplicationPad3d', 'ConstantPad1d', 'ConstantPad3d', 
    'ELU', 'Hardshrink', 'Hardtanh', 'LogSigmoid', 'MultiheadAttention', 
    'RReLU', 'SELU', 'CELU', 'GELU', 'Softshrink', 'Softsign', 'Tanhshrink', 
    'Threshold', 'Softmin', 'Softmax2d', 'LogSoftmax', 'AdaptiveLogSoftmaxWithLoss', 
    'BatchNorm3d', 'SyncBatchNorm', 'InstanceNorm1d', 'InstanceNorm3d', 'LocalResponseNorm', 
    'RNNBase', 'RNN', 'LSTM', 'GRU', 'RNNCell', 'LSTMCell', 'GRUCell', 'Transformer', 'TransformerEncoder', 
    'TransformerDecoder', 'TransformerEncoderLayer', 'TransformerDecoderLayer', 'Identity', 'Bilinear', 
    'Dropout3d', 'AlphaDropout', 'EmbeddingBag', 'CosineSimilarity', 'PairwiseDistance', 'CTCLoss', 'NLLLoss', 'PoissonNLLLoss', 'KLDivLoss', 'BCEWithLogitsLoss', 
    'MarginRankingLoss', 'HingeEmbeddingLoss', 'MultiLabelMarginLoss', 'SmoothL1Loss', 'SoftMarginLoss', 
    'MultiLabelSoftMarginLoss', 'CosineEmbeddingLoss', 'MultiMarginLoss', 'TripletMarginLoss', 'UpsamplingNearest2d', 
    'UpsamplingBilinear2d', 'DataParallel', 'DistributedDataParallel', 'clip_grad_norm_', 'clip_grad_value_', 
    'parameters_to_vector', 'vector_to_parameters', 'BasePruningMethod', 'PruningContainer', 'Identity', 
    'RandomUnstructured', 'L1Unstructured', 'RandomStructured', 'LnStructured', 'CustomFromMask', 'identity', 
    'random_unstructured', 'l1_unstructured', 'random_structured', 'ln_structured', 'global_unstructured', 
    'custom_from_mask', 'remove', 'is_pruned', 'weight_norm', 'remove_weight_norm', 'spectral_norm', 
    'remove_spectral_norm', 'PackedSequence', 'pack_padded_sequence', 'pad_packed_sequence', 'pad_sequence', 'pack_sequence'
]

support_ops = {}
for key in pjmap.keys():
    module = pjmap[key]['jittor']['module']
    name = pjmap[key]['jittor']['name']
    if module == 'nn':
        support_ops[key] = name

def raise_unsupport(name):
    raise RuntimeError(f'{name} is not supported in Jittor yet. We will appreciate it if you provide an implementation of {name} and make pull request at https://github.com/Jittor/jittor.')

def replace(a):
    if hasattr(a, "attr") and a.attr in unsupport_ops:
        raise_unsupport(a.attr)
    
    if hasattr(a, "id") and a.id in unsupport_ops:
        raise_unsupport(a.id)

    if hasattr(a, "attr"):
        if a.attr in support_ops.keys(): a.attr = support_ops[a.attr]
        
    if hasattr(a, "id"):
        if a.id in support_ops.keys(): a.id = support_ops[a.id]

import_flag = []
def convert(code):
    ''' Model code converter, example:

    from jittor.utils.pytorch_converter import convert
    pytorch_code = """
    class Model(nn.Module):
        def __init__(self):
            super().__init__()
            self.conv1 = nn.Conv2d(1, 10, 3)
            self.conv2 = nn.Conv2d(10, 32, 3)
            self.fc = nn.Linear(1200, 100)
        
        def forward(self, x):
            x = self.conv1(x)
            x = self.conv2(x)
            x = x.view(x.size(0), -1)
            x = self.fc(x)
            return x
    """
    jittor_code = convert(pytorch_code)
    print("## Generate Jittor code:", jittor_code)
    exec(jittor_code)
    model = Model()
    print("## Jittor model:", model)
    '''
    a = ast.parse(code)
    dfs(a)
    a.body.insert(0, ast.parse('import jittor as jt').body[0])
    if 'init' not in import_flag:
        a.body.insert(1, ast.parse('from jittor import init').body[0])
    if 'nn' not in import_flag:
        a.body.insert(2, ast.parse('from jittor import nn').body[0])
    return astunparse.unparse(a)

def convert_(prefix, func_name, ags, kws):
    info = pjmap[func_name]
    p_prefix = info['pytorch']['prefix'] if 'prefix' in info['pytorch'].keys() else None
    if p_prefix is not None and prefix in p_prefix:
        p_ags = info['pytorch']['args_prefix']
        j_ags = info['jittor']['args_prefix']
    else:
        p_ags = info['pytorch']['args']
        j_ags = info['jittor']['args']
    if 'delete' in info.keys():
        delete = info['delete']
    else:
        delete = None
    j_prefix = info['jittor']['prefix'] if 'prefix' in info['jittor'].keys() else None
    j_module = info['jittor']['module']
    j_name = info['jittor']['name']
    links = info['links']
    extras = info['extras']
    jj_ags = []
    jj_kws = {}
    pp_ags = []
    pp_kws = {}
    if j_ags == '' and p_ags == '':
        # no args in Pytorch and Jittor.
        if p_prefix is None:
            return f"{j_module}.{j_name}()"
        else:
            if prefix in p_prefix:
                return f"{j_prefix}.{j_name}()"
            else:
                return f"{prefix}.{j_name}()"
    else:
        j_ags = j_ags.replace(' ','').split(',')
        for j_ag in j_ags:
            if '=' in j_ag:
                k,v = j_ag.split('=')
                jj_kws[k] = v
            else:
                jj_ags.append(j_ag)
        p_ags = p_ags.replace(' ','').split(',')
        for p_ag in p_ags:
            if '=' in p_ag:
                k,v = p_ag.split('=')
                pp_kws[k] = v
            else:
                pp_ags.append(p_ag)
        if len(jj_ags) == 0 and len(pp_ags) != 0:
            raise AttributeError(f"{func_name} in Jittor has no Attribute {pp_ags[0]}")
    if delete is not None:
        for d in delete:
            if d in pp_ags:
                jj_ags.append(d)
            if d in pp_kws.keys():
                jj_kws[d] = None
    if len(pp_ags) > len(ags) + len(kws):
        raise RuntimeError(f'There are needed {len(pp_ags) + len(list(pp_kws.keys()))} args in Pytorch {func_name} function, but you only provide {len(ags) + len(kws)}')
    ags_ = []
    for i in range(len(pp_ags)):
        if i < len(ags):
            if '*' in pp_ags[i]:
                ags_.append('(' + ', '.join(ags[i:]) + ')')
                ags = ags_
                break
            else:
                ags_.append(ags[i])
        else:
            break
    if len(pp_ags) + len(list(pp_kws.keys())) < len(ags) + len(kws):
        raise RuntimeError(f'There are only {len(pp_ags) + len(list(pp_kws.keys()))} args in Pytorch {func_name} function, but you provide {len(ags) + len(kws)}')
    j_ags_flag = np.zeros(len(jj_ags))
    j_ags_values = {}
    j_kws_values = {}
    for i,ag in enumerate(ags):
        if len(pp_ags) == 0:
            ag_name = list(pp_kws.keys())[i]
        elif i < len(pp_ags):
            ag_name = pp_ags[i]
        elif i >= len(pp_ags) and (i-len(pp_ags)) <= len(list(pp_kws.keys())):
            ag_name = list(pp_kws.keys())[i-len(pp_ags)]
        else:
            raise RuntimeError(f'The args number is not matc{func_name} in Jittor has no Attribute {ag_name}')
        if ag_name in links.keys():
            ag_name = links[ag_name]
        if ag_name in jj_ags:
            j_ags_flag[jj_ags.index(ag_name)] = 1
            j_ags_values[str(jj_ags.index(ag_name))] = ag
        elif ag_name in jj_kws.keys():
            j_kws_values[ag_name] = ag
        else:
            raise AttributeError(f'{func_name} in Jittor has no Attribute {ag_name}')
    for i,kw in enumerate(kws):
        kw_name, kw_value = kw.split('=')
        if kw_name in links.keys():
            kw_name = links[kw_name]
        if kw_name in jj_ags:
            j_ags_flag[jj_ags.index(kw_name)] = 1
            j_ags_values[str(jj_ags.index(kw_name))] = kw_value
        elif kw_name in jj_kws.keys():
            j_kws_values[kw_name] = kw_value
        else:
            raise AttributeError(f'{func_name} in Jittor has no Attribute {kw_name}')
    len_jj_ags = len(jj_ags) if len(jj_ags) == 0 or jj_ags[0] != '' else 0
    if j_ags_flag.sum() < len_jj_ags:
        missing_args = []
        for i in range(len(jj_ags)):
            if j_ags_flag[i] == 0:
                missing_args.append(jj_ags[i])
        raise AttributeError(f"the needed args of {func_name} in Jittor is {', '.join(jj_ags)}, so you need to give value of {', '.join(missing_args)}.")
    if extras:
        for k in extras.keys():
            if k in jj_ags:
                j_ags_values[str(jj_ags.index(k))] = extras[k]
            elif k in jj_kws.keys():
                j_kws_values[k] = extras[k]
            else:
                raise AttributeError(f"there is not attribute named {k} in Jittor {func_name}, you should delete it in {func_name} extras.")
    if delete is not None:
        for d in delete:
            if d in j_ags_values:
                j_ags_values.remove(d)
            if d in j_kws_values.keys():
                j_kws_values.pop(d)
    j_ags_ = [j_ags_values[str(i)] for i in range(len(list(j_ags_values.keys())))]
    j_kws_ = [key + "=" + j_kws_values[key] for key in j_kws_values.keys()]
    j_func = f"{j_module}.{j_name}({', '.join(j_ags_+j_kws_)})"
    if p_prefix is None:
        return f"{j_module}.{j_name}({', '.join(j_ags_+j_kws_)})"
    else:
        if prefix in p_prefix:
            return f"{j_prefix}.{j_name}({', '.join(j_ags_+j_kws_)})"
        else:
            return f"{prefix}.{j_name}({', '.join(j_ags_+j_kws_)})"
    return j_func

def dfs(a):
    if isinstance(a, ast.Import):
        if 'torch' in astunparse.unparse(a) and 'init' in astunparse.unparse(a):
            import_flag.append('init')
            return ast.parse('from jittor import init').body[0]
        if 'torch' in astunparse.unparse(a) and a.names[0].asname == 'nn':
            import_flag.append('nn')
            return ast.parse('from jittor import nn').body[0]
        if 'torch' in a.names[0].name: 
            return 'delete'
    elif isinstance(a, ast.ImportFrom):
        if 'torch' in a.module:
            return 'delete'
    elif isinstance(a, ast.Call):
        for idx, ag in enumerate(a.args): 
            ret = dfs(ag)
            if ret is not None:
                a.args[idx] = ret
        for idx, kw in enumerate(a.keywords): 
            ret = dfs(kw)
            if ret is not None:
                a.keywords[idx] = ret
        func = astunparse.unparse(a.func).strip('\n').split('.')
        prefix = '.'.join(func[0:-1])
        func_name = func[-1]
        if func_name in unsupport_ops:
            raise_unsupport(func_name)
        if func_name in pjmap.keys():
            ags = [astunparse.unparse(ag).strip('\n') for ag in a.args]
            kws = [astunparse.unparse(kw).strip('\n') for kw in a.keywords]
            ret = convert_(prefix, func_name, ags, kws)
            return ast.parse(ret).body[0].value
        if ".load_state_dict" in astunparse.unparse(a.func):
            a.func.attr = 'load_parameters'
        if astunparse.unparse(a.func).strip('\n').endswith(".size"):
            ags = [astunparse.unparse(ag).strip('\n') for ag in a.args]
            if len(ags) != 0:
                con = astunparse.unparse(a.func).split('.size')[0] + '.shape[' + ','.join(ags) + ']'
            else:
                con = astunparse.unparse(a.func).replace('size', 'shape')
            return ast.parse(con).body[0].value
    elif isinstance(a, ast.Expr): pass
    elif isinstance(a, ast.Attribute) or isinstance(a, ast.Name): replace(a)
    elif isinstance(a, ast.FunctionDef):
        if a.name == 'forward': a.name = 'execute'
    if hasattr(a, '__dict__'):
        for k in a.__dict__.keys():
            if isinstance(a.__dict__[k], list):
                delete_flag = []
                for i,a_ in enumerate(a.__dict__[k]):
                    ret = dfs(a_)
                    if ret is 'delete':
                        delete_flag.append(True)
                        continue
                    if ret is not None:
                        a.__dict__[k][i] = ret
                    delete_flag.append(False)
                tmp = [a_ for i,a_ in enumerate(a.__dict__[k]) if delete_flag[i] == False]
                a.__dict__[k] = tmp
            else:
                ret = dfs(a.__dict__[k])
                if ret is not None:
                    a.__dict__[k] = ret