# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import os
import re
from itertools import chain
from pathlib import Path

import yaml
from jinja2 import Environment, FileSystemLoader, StrictUndefined

from filters import (
    to_op_attr_type,
    to_opmaker_name,
    to_opmaker_name_cstr,
    to_pascal_case,
)
from tests import (
    is_base_api,
    is_vec,
    is_scalar,
    is_initializer_list,
    supports_inplace,
    supports_no_need_buffer,
)
from filters import to_input_name, cartesian_prod_mapping
from parse_utils import to_named_dict

file_loader = FileSystemLoader(Path(__file__).parent / "templates")
env = Environment(
    loader=file_loader,
    keep_trailing_newline=True,
    trim_blocks=True,
    lstrip_blocks=True,
    undefined=StrictUndefined,
    extensions=['jinja2.ext.do'],
)
env.filters["to_op_attr_type"] = to_op_attr_type
env.filters["to_opmaker_name"] = to_opmaker_name
env.filters["to_pascal_case"] = to_pascal_case
env.filters["to_input_name"] = to_input_name
env.filters["to_opmaker_name_cstr"] = to_opmaker_name_cstr
env.filters["cartesian_prod_mapping"] = cartesian_prod_mapping
env.tests["base_api"] = is_base_api
env.tests["vec"] = is_vec
env.tests["scalar"] = is_scalar
env.tests["initializer_list"] = is_initializer_list
env.tests["supports_inplace"] = supports_inplace
env.tests["supports_no_need_buffer"] = supports_no_need_buffer


def restruct_io(api):
    api["input_dict"] = to_named_dict(api["inputs"])
    api["attr_dict"] = to_named_dict(api["attrs"])
    api["output_dict"] = to_named_dict(api["outputs"])
    return api


# replace name of op and params for OpMaker
def replace_compat_name(api_op_map, forward_api_dict, backward_api_dict):
    def get_api_and_op_name(api_item):
        names = api_item.split('(')
        if len(names) == 1:
            return names[0].strip(), names[0].strip()
        else:
            return names[0].strip(), names[1].split(')')[0].strip()

    for api_args in api_op_map:
        api_name, op_name = get_api_and_op_name(api_args['op'])
        if api_name not in forward_api_dict:
            continue
        forward_api_item = forward_api_dict[api_name]
        has_backward = True if forward_api_item['backward'] else False
        if has_backward:
            backward_api_item = backward_api_dict[forward_api_item['backward']]
        if api_name != op_name:
            forward_api_item['op_name'] = op_name
        if 'backward' in api_args and has_backward:
            bw_api_name, bw_op_name = get_api_and_op_name(
                api_args['backward'].split(',')[0]
            )
            forward_api_item['backward'] = bw_op_name
            backward_api_item['op_name'] = bw_op_name

        key_set = ['inputs', 'attrs', 'outputs']
        args_map = {}
        for key in key_set:
            if key in api_args:
                args_map.update(api_args[key])
                for args_item in forward_api_item[key]:
                    if args_item['name'] in api_args[key]:
                        args_item['name'] = api_args[key][args_item['name']]
                if has_backward:
                    for args_item in backward_api_item['forward'][key]:
                        if args_item['name'] in api_args[key]:
                            args_item['name'] = api_args[key][args_item['name']]
        forward_api_item['infer_meta']['param'] = [
            args_map[param] if param in args_map else param
            for param in forward_api_item['infer_meta']['param']
        ]
        forward_api_item['kernel']['param'] = [
            args_map[param] if param in args_map else param
            for param in forward_api_item['kernel']['param']
        ]
        if forward_api_item['kernel']['data_type']:
            forward_api_item['kernel']['data_type']['candidates'] = [
                args_map[param] if param in args_map else param
                for param in forward_api_item['kernel']['data_type'][
                    'candidates'
                ]
            ]
        if forward_api_item['kernel']['backend']:
            forward_api_item['kernel']['backend']['candidates'] = [
                args_map[param] if param in args_map else param
                for param in forward_api_item['kernel']['backend']['candidates']
            ]
        if forward_api_item['kernel']['layout']:
            forward_api_item['kernel']['layout']['candidates'] = [
                args_map[param] if param in args_map else param
                for param in forward_api_item['kernel']['layout']['candidates']
            ]
        if forward_api_item['inplace']:
            inplace_map = {}
            for key, val in forward_api_item['inplace'].items():
                if key in args_map:
                    key = args_map[key]
                if val in args_map:
                    val = args_map[val]
                key, val = val, key
                inplace_map[key] = val
            forward_api_item['inplace'] = inplace_map

        if has_backward:
            for args_item in backward_api_item['inputs']:
                if args_item['name'] in args_map:
                    args_item['name'] = args_map[args_item['name']]
                elif (
                    args_item['name'].endswith('_grad')
                    and args_item['name'][:-5] in args_map
                ):
                    args_map[args_item['name']] = (
                        args_map[args_item['name'][:-5]] + '_grad'
                    )
                    args_item['name'] = args_map[args_item['name']]
            for args_item in backward_api_item['attrs']:
                if args_item['name'] in args_map:
                    args_item['name'] = args_map[args_item['name']]
            for args_item in backward_api_item['outputs']:
                if (
                    args_item['name'].endswith('_grad')
                    and args_item['name'][:-5] in args_map
                ):
                    args_map[args_item['name']] = (
                        args_map[args_item['name'][:-5]] + '_grad'
                    )
                    args_item['name'] = args_map[args_item['name']]

            if 'invoke' in backward_api_item:
                backward_api_item['invoke']['args'] = [
                    args_map[param.strip()]
                    if param.strip() in args_map
                    else param.strip()
                    for param in backward_api_item['invoke']['args'].split(',')
                ]
                continue

            backward_api_item['infer_meta']['param'] = [
                args_map[param] if param in args_map else param
                for param in backward_api_item['infer_meta']['param']
            ]
            backward_api_item['kernel']['param'] = [
                args_map[param] if param in args_map else param
                for param in backward_api_item['kernel']['param']
            ]
            if backward_api_item['kernel']['data_type']:
                backward_api_item['kernel']['data_type']['candidates'] = [
                    args_map[param] if param in args_map else param
                    for param in backward_api_item['kernel']['data_type'][
                        'candidates'
                    ]
                ]
            if backward_api_item['kernel']['backend']:
                backward_api_item['kernel']['backend']['candidates'] = [
                    args_map[param] if param in args_map else param
                    for param in backward_api_item['kernel']['backend'][
                        'candidates'
                    ]
                ]
            if backward_api_item['kernel']['layout']:
                backward_api_item['kernel']['layout']['candidates'] = [
                    args_map[param] if param in args_map else param
                    for param in backward_api_item['kernel']['layout'][
                        'candidates'
                    ]
                ]
            if backward_api_item['no_need_buffer']:
                backward_api_item['no_need_buffer'] = [
                    args_map[param] if param in args_map else param
                    for param in backward_api_item['no_need_buffer']
                ]


def process_invoke_op(forward_api_dict, backward_api_dict):
    for bw_api in backward_api_dict.values():
        if 'invoke' in bw_api:
            invoke_op = bw_api['invoke']['func']
            args_list = bw_api['invoke']['args']
            args_index = 0
            if invoke_op in forward_api_dict:
                reuse_op = forward_api_dict[invoke_op]
                bw_api['invoke']['inputs'] = []
                bw_api['invoke']['attrs'] = []
                bw_api['invoke']['outputs'] = []
                for input_item in reuse_op['inputs']:
                    bw_api['invoke']['inputs'].append(
                        {
                            'name': input_item['name'],
                            'value': args_list[args_index],
                        }
                    )
                    args_index = args_index + 1
                for attr in reuse_op['attrs']:
                    if args_index < len(args_list):
                        attr_value = (
                            f"this->GetAttr(\"{args_list[args_index]}\")"
                            if args_list[args_index] in bw_api['attr_dict']
                            else args_list[args_index]
                        )
                        bw_api['invoke']['attrs'].append(
                            {'name': attr['name'], 'value': attr_value}
                        )
                        args_index = args_index + 1
                    else:
                        break
                for idx, output_item in enumerate(reuse_op['outputs']):
                    bw_api['invoke']['outputs'].append(
                        {
                            'name': output_item['name'],
                            'value': bw_api['outputs'][idx]['name'],
                        }
                    )


def main(
    ops_yaml_path,
    backward_yaml_path,
    op_compat_yaml_path,
    op_version_yaml_path,
    output_op_path,
    output_arg_map_path,
):
    with open(ops_yaml_path, "rt") as f:
        apis = yaml.safe_load(f)
        apis = [restruct_io(api) for api in apis]
    forward_api_dict = to_named_dict(apis)

    with open(backward_yaml_path, "rt") as f:
        backward_apis = yaml.safe_load(f)
        backward_apis = [restruct_io(api) for api in backward_apis]
    backward_api_dict = to_named_dict(backward_apis)

    with open(op_version_yaml_path, "rt") as f:
        api_versions = yaml.safe_load(f)
    # add api version info into api
    for api_version in api_versions:
        forward_api_dict[api_version['op']]['version'] = api_version['version']

    with open(op_compat_yaml_path, "rt") as f:
        api_op_map = yaml.safe_load(f)

    for api in apis:
        api['op_name'] = api['name']
    for bw_api in backward_apis:
        bw_api['op_name'] = bw_api['name']

    replace_compat_name(api_op_map, forward_api_dict, backward_api_dict)

    # prepare for invoke case
    process_invoke_op(forward_api_dict, backward_api_dict)

    # fill backward field for an api if another api claims it as forward
    for name, backward_api in backward_api_dict.items():
        forward_name = backward_api["forward"]["name"]
        if forward_name in backward_api_dict:
            forward_api = backward_api_dict[forward_name]
            if forward_api["backward"] is None:
                forward_api["backward"] = name

    api_dict = {}
    api_dict.update(forward_api_dict)
    api_dict.update(backward_api_dict)

    if len(apis) == 0 and len(backward_apis) == 0:
        if os.path.isfile(output_op_path):
            os.remove(output_op_path)
        if os.path.isfile(output_arg_map_path):
            os.remove(output_arg_map_path)
        return

    op_template = env.get_template('op.c.j2')
    with open(output_op_path, "wt") as f:
        msg = op_template.render(
            apis=apis, backward_apis=backward_apis, api_dict=api_dict
        )
        f.write(msg)

    ks_template = env.get_template('ks.c.j2')
    with open(output_arg_map_path, 'wt') as f:
        msg = ks_template.render(apis=apis, backward_apis=backward_apis)
        f.write(msg)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Generate operator file from api yaml."
    )
    parser.add_argument(
        '--ops_yaml_path', type=str, help="parsed ops yaml file."
    )
    parser.add_argument(
        '--backward_yaml_path', type=str, help="parsed backward ops yaml file."
    )
    parser.add_argument(
        '--op_compat_yaml_path', type=str, help="ops args compat yaml file."
    )
    parser.add_argument(
        '--op_version_yaml_path', type=str, help="ops version yaml file."
    )
    parser.add_argument(
        "--output_op_path", type=str, help="path to save generated operators."
    )
    parser.add_argument(
        "--output_arg_map_path",
        type=str,
        help="path to save generated argument mapping functions.",
    )

    args = parser.parse_args()
    main(
        args.ops_yaml_path,
        args.backward_yaml_path,
        args.op_compat_yaml_path,
        args.op_version_yaml_path,
        args.output_op_path,
        args.output_arg_map_path,
    )
