/**
 * \file dnn/test/common/accuracy_shake_checker.h
 * MegEngine is Licensed under the Apache License, Version 2.0 (the "License")
 *
 * Copyright (c) 2014-2021 Megvii Inc. All rights reserved.
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 */

#pragma once

#include <vector>
#include "megdnn/oprs.h"
#include "src/common/conv_bias.h"
#include "src/common/utils.h"
#include "test/common/checker.h"
#include "test/common/index.h"

namespace megdnn {
namespace test {

namespace {

template <class Opr>
struct BatchTrait {
    //! index of batch in tensor, 3 for CHWN4 e.g.
    static size_t index_of_batch(const typename Opr::Param&) { return 0; }

    //! indices contain batch in inputs and outputs, src(0) dst(2) for conv e.g.
    static std::vector<size_t> indices_contain_batch;

    static std::vector<size_t> indices_contain_batch_broadcast;
};

template <class Opr>
std::vector<size_t> BatchTrait<Opr>::indices_contain_batch = {};
template <class Opr>
std::vector<size_t> BatchTrait<Opr>::indices_contain_batch_broadcast  = {};

#define DEFAULT_INDEX_OF_BATCH(opr) \
    static size_t index_of_batch(const opr::Param&) { return 0; }

#define CONV_INDEX_OF_BATCH(opr)                        \
    static size_t index_of_batch(const opr::Param& p) { \
        if (p.format == opr::Param::Format::CHWN4) {    \
            return 3;                                   \
        }                                               \
        return 0;                                       \
    }

#define OPR_WITHOUT_INPUT_BROADCAST(INDEX_OF_BATCH, opr, idxs, idxs_brdcst) \
    template <>                                                             \
    struct BatchTrait<opr> {                                                \
        INDEX_OF_BATCH(opr)                                                 \
        static std::vector<size_t> indices_contain_batch;                   \
        static std::vector<size_t> indices_contain_batch_broadcast;         \
    };                                                                      \
    std::vector<size_t> BatchTrait<opr>::indices_contain_batch = idxs;      \
    std::vector<size_t> BatchTrait<opr>::indices_contain_batch_broadcast =  \
            idxs_brdcst;

OPR_WITHOUT_INPUT_BROADCAST(DEFAULT_INDEX_OF_BATCH,
                            megdnn::Convolution3DForward,
                            (std::initializer_list<size_t>{0, 2}), {})
OPR_WITHOUT_INPUT_BROADCAST(DEFAULT_INDEX_OF_BATCH,
                            megdnn::Convolution3DBackwardData,
                            (std::initializer_list<size_t>{1, 2}), {})
OPR_WITHOUT_INPUT_BROADCAST(DEFAULT_INDEX_OF_BATCH,
                            megdnn::Convolution3DBackwardFilter,
                            (std::initializer_list<size_t>{0, 1}), {})
OPR_WITHOUT_INPUT_BROADCAST(DEFAULT_INDEX_OF_BATCH, megdnn::BatchedMatrixMul,
                            (std::initializer_list<size_t>{0, 1, 2}), {})

OPR_WITHOUT_INPUT_BROADCAST(CONV_INDEX_OF_BATCH, megdnn::ConvolutionForward,
                            (std::initializer_list<size_t>{0, 2}), {})
OPR_WITHOUT_INPUT_BROADCAST(CONV_INDEX_OF_BATCH,
                            megdnn::ConvolutionBackwardData,
                            (std::initializer_list<size_t>{1, 2}), {})
OPR_WITHOUT_INPUT_BROADCAST(CONV_INDEX_OF_BATCH,
                            megdnn::ConvolutionBackwardFilter,
                            (std::initializer_list<size_t>{0, 1}), {})
OPR_WITHOUT_INPUT_BROADCAST(CONV_INDEX_OF_BATCH, megdnn::LocalShareForward,
                            (std::initializer_list<size_t>{0, 2}), {})
OPR_WITHOUT_INPUT_BROADCAST(CONV_INDEX_OF_BATCH, megdnn::LocalShareBackwardData,
                            (std::initializer_list<size_t>{1, 2}), {})
OPR_WITHOUT_INPUT_BROADCAST(CONV_INDEX_OF_BATCH,
                            megdnn::LocalShareBackwardFilter,
                            (std::initializer_list<size_t>{0, 1}), {})
OPR_WITHOUT_INPUT_BROADCAST(CONV_INDEX_OF_BATCH, megdnn::DeformableConvForward,
                            (std::initializer_list<size_t>{0, 2, 3, 4}), {})
OPR_WITHOUT_INPUT_BROADCAST(
        CONV_INDEX_OF_BATCH, megdnn::DeformableConvBackwardData,
        (std::initializer_list<size_t>{0, 2, 3, 4, 5, 6, 7}), {})
OPR_WITHOUT_INPUT_BROADCAST(CONV_INDEX_OF_BATCH,
                            megdnn::DeformableConvBackwardFilter,
                            (std::initializer_list<size_t>{0, 1, 2, 3}), {})
OPR_WITHOUT_INPUT_BROADCAST(CONV_INDEX_OF_BATCH, megdnn::BatchConvBiasForward,
                            (std::initializer_list<size_t>{0, 1, 2, 3, 4}), {})
OPR_WITHOUT_INPUT_BROADCAST(CONV_INDEX_OF_BATCH, megdnn::ConvBiasForward,
                            (std::initializer_list<size_t>{0, 3, 4}), {2})
#undef OPR_WITHOUT_INPUT_BROADCAST
#undef DEFAULT_INDEX_OF_BATCH
#undef CONV_INDEX_OF_BATCH

template <class Opr>
struct LayoutsModifier {
    static void on(TensorLayoutArray& layouts, const typename Opr::Param& p,
                   size_t new_batch_size) {
        size_t batch_index = BatchTrait<Opr>::index_of_batch(p);
        for (size_t index : BatchTrait<Opr>::indices_contain_batch) {
            layouts.at(index)[batch_index] = new_batch_size;
        }

        for (size_t index : BatchTrait<Opr>::indices_contain_batch_broadcast) {
            if (!check_bias_share_in_channel(layouts.at(index), p.format)) {
                layouts.at(index)[batch_index] = new_batch_size;
            }
        }
    }
};

#define OPR_NO_BIAS(opr)                                                      \
    template <>                                                               \
    struct LayoutsModifier<opr> {                                             \
        static void on(TensorLayoutArray& layouts,                            \
                       const typename opr::Param& p, size_t new_batch_size) { \
            size_t batch_index = BatchTrait<opr>::index_of_batch(p);          \
            for (size_t index : BatchTrait<opr>::indices_contain_batch) {     \
                layouts.at(index)[batch_index] = new_batch_size;              \
            }                                                                 \
        }                                                                     \
    };

OPR_NO_BIAS(megdnn::Convolution3D)
OPR_NO_BIAS(megdnn::BatchedMatrixMul)
#undef OPR_NO_BIAS

template <>
struct LayoutsModifier<megdnn::MatrixMul> {
public:
    static void on(TensorLayoutArray& layouts,
                   const megdnn::MatrixMul::Param& p,
                   size_t new_batch_size) {
        assert(!p.transposeA && !p.transposeB);
        MEGDNN_MARK_USED_VAR(p);
        layouts.at(0)[0] = new_batch_size;
        layouts.at(2)[0] = new_batch_size;
    }
};

template <class Opr, typename OprAlgoProxy = OprAlgoProxy<Opr>>
class AlgoGenerator {
public:
    AlgoGenerator(ExecutionPolicyAlgoName name)
            : m_policy_name{name} {}

    std::vector<Algorithm::Info::Desc> operator()(
            Opr* opr, const CheckerHelper::TensorValueArray& arr) {
        TensorLayoutArray layouts;
        for (auto&& val : arr) {
            layouts.push_back(val.layout);
        }
        std::vector<Algorithm::Info::Desc> ret;
        megdnn_assert(layouts.size() == OprTrait<Opr>::arity);
        for (auto algo_info :
             AlgoProxy<Opr, OprTrait<Opr>::arity>::get_all_algorithms_info(
                     opr, layouts)) {
            if (!(algo_info.attribute &
                 AlgoAttribute::ACCURACY_DEPEND_ON_BATCH) &&
                std::regex_match(
                        algo_info.desc.name,
                        std::regex("(.*)(" + m_policy_name.name + ")(.*)"))) {
                ret.push_back(algo_info.desc);
            } else {
                continue;
            }
        }
        return ret;
    }

private:
    ExecutionPolicyAlgoName m_policy_name;
};

}  // namespace

::testing::AssertionResult __assert_tensor_binary_eq(
        const char* expr0, const char* expr1, const char* expr2,
        const TensorND& v0, const TensorND& v1,
        const Algorithm::Info::Desc& algo);

template <typename Opr, typename Proxy = OprProxy<Opr>>
class AccuracyShakeChecker : public CheckerHelper {
public:
    static constexpr int arity_in = OprArityTrait<Opr>::arity_in;
    using Param = typename Opr::Param;
    using BeforeExecCallback = std::function<std::vector<Algorithm::Info::Desc>(
            Opr*, const TensorValueArray&)>;
    AccuracyShakeChecker(Handle* handle, bool check_dispatch = false)
            : CheckerHelper(handle, check_dispatch),
              m_before_exec_callback{AlgoGenerator<Opr>("")},
              m_param(Param()) {}

    TensorLayoutArray make_layouts(const TensorShapeArray& shapes) {
        TensorLayoutArray layouts(shapes.size());
        for (size_t i = 0; i < shapes.size(); ++i) {
            DType dt = (m_dtype.find(i) != m_dtype.end() ? m_dtype[i]
                                                         : dtype::Float32());
            TensorFormat fmt =
                    (m_fmt.find(i) != m_fmt.end() ? m_fmt[i] : TensorFormat{});
            layouts[i] = TensorLayout(shapes[i], dt, fmt);
        }
        return layouts;
    }

    /*!
     * \brief execute opr on current param/dtype/rng config
     * \param shapes input/output shapes, which would be passed as
     *      arguments to Opr::deduce_layout
     *
     * Checker would construct TensorLayout vectors from shapes and dtypes,
     * and call exec(TensorLayoutArray &).
     */
    AccuracyShakeChecker& exec(const TensorShapeArray& shapes) {
        exec(make_layouts(shapes));
        return *this;
    }

    void exec(TensorLayoutArray layouts);

    AccuracyShakeChecker& set_param(Param p) {
        m_param = p;
        opr()->param() = p;
        return *this;
    }
    AccuracyShakeChecker& set_dtype(size_t idx, DType dtype) {
        m_dtype[idx] = dtype;
        return *this;
    }
    AccuracyShakeChecker& set_rng(size_t idx, RNG* rng) {
        m_rng[idx] = rng;
        return *this;
    }

    //! set a callback to be invoked before executing the operator
    AccuracyShakeChecker& set_before_exec_callback(
            const BeforeExecCallback& cb) {
        m_before_exec_callback = cb;
        return *this;
    }

    AccuracyShakeChecker& reset_before_exec_callback() {
        m_before_exec_callback = nullptr;
        return *this;
    }

    //! get the opr impl so setting other than param() can be modified
    Opr* opr() {
        if (!m_opr_cur) {
            m_opr_cur = m_handle_cur->create_operator<Opr>();
        }
        return m_opr_cur.get();
    }

private:
    BeforeExecCallback m_before_exec_callback;
    Param m_param;
    Proxy m_proxy;
    std::unique_ptr<Opr> m_opr_cur;
    std::shared_ptr<TensorValueArray> m_tensors_cur_host,
            m_tensors_single_batch_host;

    void init_host_values();

    void check_tensors_ignore_batch(
            const TensorValueArray& tensors_single_batch,
            const TensorValueArray& tensors, const Algorithm::Info::Desc& desc);
};

template <typename Opr, typename Proxy>
void AccuracyShakeChecker<Opr, Proxy>::exec(TensorLayoutArray layouts) {
    auto opr_cur = this->opr();
    opr_cur->param() = m_param;

    m_proxy.deduce_layout(opr_cur, layouts);

    TensorLayoutArray layouts_single_batch = layouts;
    for (size_t i=0; i<layouts_single_batch.size(); ++i) {
        ASSERT_TRUE(layouts[i].is_physical_contiguous())
                << "layouts should be physical contiguous "
                << layouts[i].to_string();
    }

    ASSERT_TRUE(0 == BatchTrait<Opr>::index_of_batch(opr_cur->param()))
                << "index of batch should be 0 ";

    LayoutsModifier<Opr>::on(layouts_single_batch, opr_cur->param(), 1);

    // allocate input
    auto tensors_single_batch_storage =
            alloc_tensors(m_handle_cur, layouts_single_batch, 0);
    m_tensors_single_batch_host =
            alloc_tensors(m_handle_naive.get(), layouts_single_batch, 0);
    auto tensors_cur_storage = alloc_tensors(m_handle_cur, layouts, 0);
    m_tensors_cur_host =
            alloc_tensors(m_handle_naive.get(), layouts, 0);
    auto &&tensors_single_batch = *tensors_single_batch_storage;
    auto &&tensors_single_batch_host = *m_tensors_single_batch_host;
    auto &&tensors_cur = *tensors_cur_storage;
    auto &&tensors_cur_host = *m_tensors_cur_host;

    // allocate output
    auto tensors_single_batch_storage_out =
            alloc_tensors(m_handle_naive.get(), layouts_single_batch, 0);
    auto tensors_cur_storage_out =
            alloc_tensors(m_handle_naive.get(), layouts, 0);
    auto &&tensors_single_batch_out = *tensors_single_batch_storage_out;
    auto &&tensors_cur_out = *tensors_cur_storage_out;

    init_host_values();

    copy_tensors_to_device(tensors_cur, tensors_cur_host);
    copy_tensors_to_device(tensors_single_batch, tensors_single_batch_host);

    std::vector<Algorithm::Info::Desc> algo_desc;
    if (m_before_exec_callback) {
        algo_desc = m_before_exec_callback(opr_cur, tensors_cur);
    } else {
        algo_desc.push_back({});
    }
    for (size_t i = 0; i < algo_desc.size(); ++i) {
        opr_cur->execution_policy().algo = algo_desc[i];
        m_proxy.exec(opr_cur, tensors_cur);
        m_proxy.exec(opr_cur, tensors_single_batch);

        copy_tensors_from_device(tensors_cur_out, tensors_cur);
        copy_tensors_from_device(tensors_single_batch_out,
                                 tensors_single_batch);

        check_tensors_ignore_batch(tensors_single_batch_out, tensors_cur_out,
                                   algo_desc[i]);
    }
}

template <typename Opr, typename Proxy>
void AccuracyShakeChecker<Opr, Proxy>::init_host_values() {
    size_t index_of_batch = 0;
    auto &&tensors_single_batch = *m_tensors_single_batch_host;
    auto &&tensors_cur = *m_tensors_cur_host;
    for (size_t i = 0; i < arity_in; ++i) {
        auto &&tensor_single_batch = tensors_single_batch[i];
        auto &&tensor_cur = tensors_cur[i];
        auto rng = m_rng[i];
        if (!rng)
            rng = m_default_rng.get();
        rng->gen(tensor_single_batch);

        dt_byte* raw_storage_cur = static_cast<dt_byte*>(tensor_cur.raw_ptr) +
                                   tensor_cur.layout.span().low_byte;
        dt_byte* raw_storage_single_batch =
                static_cast<dt_byte*>(tensor_single_batch.raw_ptr) +
                tensor_single_batch.layout.span().low_byte;
        const size_t step = tensor_single_batch.layout.span().dist_byte();
        if (tensor_cur.layout.eq_shape(tensor_single_batch.layout)) {
            memcpy(raw_storage_cur, raw_storage_single_batch, step);
        } else {
            ASSERT_TRUE(1 == tensor_single_batch.layout[index_of_batch])
                << "bad batch size "
                << tensor_single_batch.layout[index_of_batch];
            for (size_t b=0; b<tensor_cur.layout[index_of_batch]; ++b) {
                memcpy(raw_storage_cur, raw_storage_single_batch, step);
                raw_storage_cur += step;
            }
        }
    }
}

template <typename Opr, typename Proxy>
void AccuracyShakeChecker<Opr, Proxy>::check_tensors_ignore_batch(
        const TensorValueArray& tensors_single_batch,
        const TensorValueArray& tensors, const Algorithm::Info::Desc& algo) {
    for (size_t i = 0; i < tensors_single_batch.size(); ++i) {
        if (tensors_single_batch[i].layout.ndim == 0 ||
            tensors_single_batch[i].layout.eq_shape(tensors[i].layout))
            continue;
        ASSERT_PRED_FORMAT3(::megdnn::test::__assert_tensor_binary_eq,
                            tensors_single_batch[i], tensors[i], algo);
    }
}

}  // namespace test
}  // namespace megdnn

// vim: syntax=cpp.doxygen
