/**
 * Copyright (c) 2025 Huawei Technologies Co., Ltd.
 * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
 * CANN Open Software License Agreement Version 2.0 (the "License").
 * Please refer to the License for details. You may not use this file except in compliance with the License.
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
 * See LICENSE in the root of the software repository for the full text of the License.
 */

#include "onnx_common.h"
#include "op_nn_proto_extend.h"

namespace domi {

using OpDesc = std::shared_ptr<ge::OpDesc>;
using namespace ge;
static const int INPUT_4D = 4;
static const int INPUT_5D = 5;
static const int INPUT_NUM_2 = 2;
static const int INPUT_NUM_3 = 3;
static const int ONNX_1D_ATTR_LEN = 1;
static const int ONNX_1D_ATTR_PAD_LEN = 2;

struct ConvAttr {
    std::vector<int64_t> dilations = {1, 1};
    std::vector<int64_t> strides = {1, 1};
    std::vector<int64_t> pads;
    int64_t groups;
    std::string data_format;
    int dim_size;
    int input_num;
    bool trans_2d = false;
    std::string auto_pad = "NOTSET";
};

static Status SetAttrToOp(const ge::onnx::NodeProto* node, ge::Operator& op)
{
    std::vector<int32_t> strides_list = {1, 1};
    std::vector<int32_t> dilations_list = {1, 1};
    std::vector<int32_t> pad_list;
    bool is_trans_2d = false;
    bool is_have_kernel_shape = false;
    int dim_size = INPUT_4D;
    for (const auto& attr : node->attribute()) {
        if (attr.name() == "strides" && attr.type() == ge::onnx::AttributeProto::INTS) {
            if (attr.ints_size() == ONNX_1D_ATTR_LEN) {
                strides_list.push_back(1);
                is_trans_2d = true;
            }
            for (auto i = 0; i < attr.ints_size(); ++i) {
                strides_list.push_back(attr.ints(i));
            }
            op.SetAttr("strides", strides_list);
        } else if (attr.name() == "dilations" && attr.type() == ge::onnx::AttributeProto::INTS) {
            if (attr.ints_size() == ONNX_1D_ATTR_LEN) {
                dilations_list.push_back(1);
                is_trans_2d = true;
            }
            for (auto i = 0; i < attr.ints_size(); ++i) {
                dilations_list.push_back(attr.ints(i));
            }
            op.SetAttr("dilations", dilations_list);
        } else if (attr.name() == "pads" && attr.type() == ge::onnx::AttributeProto::INTS) {
            unsigned int len = attr.ints_size();
            if (len & 1) {
                OP_LOGE("Conv", "The value lenth of pads is odd, transform failed.");
                return FAILED;
            }
            if (attr.ints_size() == ONNX_1D_ATTR_PAD_LEN) {
                pad_list.push_back(0);
                pad_list.push_back(0);
                is_trans_2d = true;
            }
            for (unsigned int i = 0; i < len / 2; i++) {
                pad_list.push_back(attr.ints(i));
                pad_list.push_back(attr.ints(i + len / 2));
            }
            op.SetAttr("pads", pad_list);
        } else if (attr.name() == "group" && attr.type() == ge::onnx::AttributeProto::INT) {
            op.SetAttr("groups", attr.i());
        } else if (attr.name() == "auto_pad" && attr.type() == ge::onnx::AttributeProto::STRING) {
            op.SetAttr("auto_pad", attr.s());
        } else if (attr.name() == "kernel_shape" && attr.type() == ge::onnx::AttributeProto::INTS) {
            is_have_kernel_shape = true;
            is_trans_2d = attr.ints_size() >= 2 ? false : true;
            dim_size = attr.ints_size() > 2 ? INPUT_5D : INPUT_4D;
        }
    }

    if (!is_have_kernel_shape) {
        if (strides_list.size() == 2 && dilations_list.size() == 2 && pad_list.empty()) {
            OP_LOGE(GetOpName(op).c_str(), "node must have attr (kernel_shape,pads,strides,dilations) at least one");
            return FAILED;
        }
        dim_size = (strides_list.size() == 5 || pad_list.size() == 6 || dilations_list.size() == 5) ? 5 : 4;
    }

    op.SetAttr("dim_size", dim_size);
    op.SetAttr("trans_2d", is_trans_2d);

    return SUCCESS;
}

static Status ParseParamsConv(const Message* op_src, ge::Operator& op)
{
    const ge::onnx::NodeProto* node = dynamic_cast<const ge::onnx::NodeProto*>(op_src);
    if (nullptr == node) {
        OP_LOGE("Conv", "Dynamic cast op_src to NodeProto failed.");
        return FAILED;
    }

    int n = node->input_size();
    op.SetAttr("input_num", n);
    op.SetAttr("name", node->name());
    op.DynamicInputRegister("args", n);
    op.DynamicOutputRegister("output", 1);
    op.SetAttr("original_type", "ai.onnx::11::Conv");

    if (SetAttrToOp(node, op) != SUCCESS) {
        OP_LOGE("Conv", "set attr to operator failed");
        return FAILED;
    }

    return SUCCESS;
}

static Status SetFormat(ge::Operator& op, const int& dims)
{
    if (dims == INPUT_4D) {
        auto ret_x = ChangeFormatFromOnnx(op, 0, ge::FORMAT_NCHW, true);
        if (ret_x != ge::GRAPH_SUCCESS) {
            OP_LOGE("Conv", "update fmap format failed.");
            return FAILED;
        }
        auto ret_w = ChangeFormatFromOnnx(op, 1, ge::FORMAT_NCHW, true);
        if (ret_w != ge::GRAPH_SUCCESS) {
            OP_LOGE("Conv", "update filter format failed.");
            return FAILED;
        }
        auto ret_y = ChangeFormatFromOnnx(op, 0, ge::FORMAT_NCHW, false);
        if (ret_y != ge::GRAPH_SUCCESS) {
            OP_LOGE("Conv", "update output format failed.");
            return FAILED;
        }
    } else if (dims == INPUT_5D) {
        auto ret_x = ChangeFormatFromOnnx(op, 0, ge::FORMAT_NCDHW, true);
        if (ret_x != ge::GRAPH_SUCCESS) {
            OP_LOGE("Conv", "update fmap format failed.");
            return FAILED;
        }
        auto ret_w = ChangeFormatFromOnnx(op, 1, ge::FORMAT_NCDHW, true);
        if (ret_w != ge::GRAPH_SUCCESS) {
            OP_LOGE("Conv", "update filter format failed.");
            return FAILED;
        }
        auto ret_y = ChangeFormatFromOnnx(op, 0, ge::FORMAT_NCDHW, false);
        if (ret_y != ge::GRAPH_SUCCESS) {
            OP_LOGE("Conv", "update output format failed.");
            return FAILED;
        }
    } else {
        OP_LOGE("Conv", "The input tensor is not 4D/5D, set format failed.");
        return FAILED;
    }
    return SUCCESS;
}

static Status GetConvAttr(const ge::Operator& op, ConvAttr& convAttr)
{
    auto ret_strides = op.GetAttr("strides", convAttr.strides);
    auto ret_pads = op.GetAttr("pads", convAttr.pads);
    auto ret_dilations = op.GetAttr("dilations", convAttr.dilations);
    op.GetAttr("auto_pad", convAttr.auto_pad);
    std::string pad_mode = convAttr.auto_pad;

    if (ret_strides != SUCCESS && ret_pads != SUCCESS && ret_dilations != SUCCESS) {
        OP_LOGW("Conv",
                "get attr of strides or pads or dilations from op failed, can not distinguish 2D/3D, use default 2D,"
                " please set one of them obviously.");
    }
    if (op.GetAttr("dim_size", convAttr.dim_size) != SUCCESS) {
        OP_LOGE("Conv", "get dim size from op failed");
        return FAILED;
    }
    if (pad_mode != "NOTSET" && pad_mode != "VALID" && convAttr.dim_size == INPUT_5D) {
        OP_LOGE("Conv",
                "The attr of auto_pad is not NOTSET/VALID for 5D, unsupported other value for now, transform failed,"
                "may cause precision error.");
        return FAILED;
    }
    if (op.GetAttr("input_num", convAttr.input_num) != SUCCESS) {
        OP_LOGE("Conv", "get number of input from op failed");
        return FAILED;
    }
    if (op.GetAttr("groups", convAttr.groups) != SUCCESS)
        convAttr.groups = 1;

    if (op.GetAttr("data_format", convAttr.data_format) != SUCCESS) {
        std::string data_format = (convAttr.strides.size() == 5 || convAttr.pads.size() == 6 ||
                                   convAttr.dilations.size() == 5) ?
                                      "NCDHW" :
                                      "NCHW";
        convAttr.data_format = data_format;
    }

    if (op.GetAttr("trans_2d", convAttr.trans_2d) != SUCCESS) {
        OP_LOGW("Conv", "get the flag of convert 1d to 2d failed, use default.");
    }

    std::vector<int64_t> strides_list_default = {1, 1, 1, 1};
    std::vector<int64_t> dilations_list_default = {1, 1, 1, 1};
    std::vector<int64_t> pad_list_default = {0, 0, 0, 0};
    if (convAttr.dim_size == INPUT_5D) {
        strides_list_default.push_back(1);
        dilations_list_default.push_back(1);
        pad_list_default.push_back(0);
        pad_list_default.push_back(0);
    }

    if (convAttr.strides.size() == 2) {
        convAttr.strides = strides_list_default;
    }
    if (convAttr.dilations.size() == 2) {
        convAttr.dilations = dilations_list_default;
    }
    if (convAttr.pads.size() == 0) {
        convAttr.pads = pad_list_default;
    }
    return SUCCESS;
}

static Status ParseOpToGraphConv(const ge::Operator& op, Graph& graph)
{
    std::string ori_name;
    if (op.GetAttr("name", ori_name) != SUCCESS) {
        OP_LOGE(GetOpName(op).c_str(), "get name from op failed.");
        return FAILED;
    }

    ConvAttr tbeAttr;
    if (GetConvAttr(op, tbeAttr) != SUCCESS) {
        OP_LOGE("Conv", "get attr value failed.");
        return FAILED;
    }

    ge::Operator dataX = op::Data(ori_name + "_dataX").set_attr_index(0);
    ge::Operator dataW = op::Data(ori_name + "_dataW").set_attr_index(1);
    std::vector<Operator> inputs{dataX, dataW};
    std::vector<std::pair<Operator, std::vector<size_t>>> outputs;
    ge::Operator conv;
    ge::Operator dataB;
    ge::Operator squeeze;
    if (tbeAttr.dim_size == INPUT_4D) {
        if (tbeAttr.trans_2d) {
            ge::Operator::OpListInt axes = {2};
            dataX = op::Unsqueeze(ori_name + "_UnsqueezeX").set_input_x(dataX).set_attr_axes(axes);
            dataW = op::Unsqueeze(ori_name + "_UnsqueezeW").set_input_x(dataW).set_attr_axes(axes);
        }
        switch (tbeAttr.input_num) {
            case INPUT_NUM_2:
                conv = op::Conv2D(ori_name + "_Conv2D")
                           .set_input_x(dataX)
                           .set_input_filter(dataW)
                           .set_attr_strides(tbeAttr.strides)
                           .set_attr_pads(tbeAttr.pads)
                           .set_attr_dilations(tbeAttr.dilations)
                           .set_attr_groups(tbeAttr.groups)
                           .set_attr_data_format(tbeAttr.data_format);
                break;
            case INPUT_NUM_3:
                dataB = op::Data(ori_name + "_dataB").set_attr_index(INPUT_NUM_3 - 1);
                inputs.push_back(dataB);
                conv = op::Conv2D(ori_name + "_Conv2D")
                           .set_input_x(dataX)
                           .set_input_filter(dataW)
                           .set_input_bias(dataB)
                           .set_attr_strides(tbeAttr.strides)
                           .set_attr_pads(tbeAttr.pads)
                           .set_attr_dilations(tbeAttr.dilations)
                           .set_attr_groups(tbeAttr.groups)
                           .set_attr_data_format(tbeAttr.data_format);
                break;
            default:
                OP_LOGE("Conv", "the num of inputs is incorrect.");
                return FAILED;
        }
        if (SetFormat(conv, tbeAttr.dim_size) != SUCCESS) {
            OP_LOGE("Conv", "set format for input and output failed.");
            return FAILED;
        }
        if (tbeAttr.trans_2d) {
            ge::Operator::OpListInt axis = {2};
            squeeze = op::Squeeze(ori_name + "_SqueezeY").set_input_x(conv).set_attr_axis(axis);
        }
    } else if (tbeAttr.dim_size == INPUT_5D) {
        switch (tbeAttr.input_num) {
            case INPUT_NUM_2:
                conv = op::Conv3D(ori_name + "_Conv3D")
                           .set_input_x(dataX)
                           .set_input_filter(dataW)
                           .set_attr_strides(tbeAttr.strides)
                           .set_attr_pads(tbeAttr.pads)
                           .set_attr_dilations(tbeAttr.dilations)
                           .set_attr_groups(tbeAttr.groups)
                           .set_attr_data_format(tbeAttr.data_format);
                break;
            case INPUT_NUM_3:
                dataB = op::Data(ori_name + "_dataB").set_attr_index(INPUT_NUM_3 - 1);
                inputs.push_back(dataB);
                conv = op::Conv3D(ori_name + "_Conv3D")
                           .set_input_x(dataX)
                           .set_input_filter(dataW)
                           .set_input_bias(dataB)
                           .set_attr_strides(tbeAttr.strides)
                           .set_attr_pads(tbeAttr.pads)
                           .set_attr_dilations(tbeAttr.dilations)
                           .set_attr_groups(tbeAttr.groups)
                           .set_attr_data_format(tbeAttr.data_format);
                break;
            default:
                OP_LOGE("Conv", "the num of inputs is incorrect.");
                return FAILED;
        }
        if (SetFormat(conv, tbeAttr.dim_size) != SUCCESS) {
            OP_LOGE("Conv", "set format for input and output failed.");
            return FAILED;
        }
    } else {
        OP_LOGE("Conv", "just support 4D or 5D input, transform failed.");
        return FAILED;
    }

    conv.SetAttr("auto_pad", tbeAttr.auto_pad);

    auto outputOp = tbeAttr.trans_2d ? squeeze : conv;
    outputs.emplace_back(outputOp, std::vector<std::size_t>{0});
    graph.SetInputs(inputs).SetOutputs(outputs);
    return SUCCESS;
}

REGISTER_CUSTOM_OP("PartitionedCall")
    .FrameworkType(ONNX)
    .OriginOpType({"ai.onnx::8::Conv", "ai.onnx::9::Conv", "ai.onnx::10::Conv", "ai.onnx::11::Conv",
                   "ai.onnx::12::Conv", "ai.onnx::13::Conv", "ai.onnx::14::Conv", "ai.onnx::15::Conv",
                   "ai.onnx::16::Conv", "ai.onnx::17::Conv", "ai.onnx::18::Conv"})
    .ParseParamsFn(ParseParamsConv)
    .ParseOpToGraphFn(ParseOpToGraphConv)
    .ImplyType(ImplyType::TVM);
} // namespace domi