/**
 * Copyright 2020 Huawei Technologies Co., Ltd
 *
 * 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.
 */

#ifndef MINDSPORE_SERVING_TEST_SERVABLE_COMMON_H
#define MINDSPORE_SERVING_TEST_SERVABLE_COMMON_H

#include <fstream>
#include <memory>
#include <set>
#include <string>
#include <vector>
#include "common/common_test.h"
#include "master/server.h"
#include "worker/worker.h"
#include "worker/notfiy_master/local_notify.h"
#include "worker/context.h"
#include "worker/local_servable/local_sevable.h"
#include "master/grpc/grpc_process.h"
#include "mindspore_serving/proto/ms_service.pb.h"

namespace mindspore {
namespace serving {

#define ExpectContainMsg(error_msg, expected_msg)                                                     \
  {                                                                                                   \
    auto error_msg_str = error_msg;                                                                   \
    EXPECT_TRUE(error_msg_str.find(expected_msg) != std::string::npos);                               \
    if (error_msg_str.find(expected_msg) == std::string::npos) {                                      \
      std::cout << "error_msg: " << error_msg_str << ", expected_msg: " << expected_msg << std::endl; \
    }                                                                                                 \
  }

class TestMasterWorker : public UT::Common {
 public:
  TestMasterWorker() = default;
  void Init(std::string servable_dir, std::string servable_name, int version_number, std::string model_file) {
    servable_dir_ = servable_dir;
    servable_name_ = servable_name;
    version_number_ = version_number;
    model_file_ = model_file;

    servable_name_path_ = servable_dir_ + "/" + servable_name_;
    version_number_path_ = servable_name_path_ + "/" + std::to_string(version_number_);
    model_name_path_ = version_number_path_ + "/" + model_file_;

    __mode_t access_mode = S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH;
    mkdir(servable_dir_.c_str(), access_mode);
    mkdir(servable_name_path_.c_str(), access_mode);
    mkdir(version_number_path_.c_str(), access_mode);
    std::ofstream fp(model_name_path_);
    fp << "model content";
    fp.close();
    model_name_path_list_.emplace(model_name_path_);
    version_number_path_list_.emplace(version_number_path_);
    servable_name_path_list_.emplace(servable_name_path_);
    servable_dir_list_.emplace(servable_dir_);
  }

  virtual void SetUp() {}
  virtual void TearDown() {
    for (auto &item : model_name_path_list_) {
      remove(item.c_str());
    }
    for (auto &item : version_number_path_list_) {
      rmdir(item.c_str());
    }
    for (auto &item : servable_name_path_list_) {
      rmdir(item.c_str());
    }
    for (auto &item : servable_dir_list_) {
      rmdir(item.c_str());
    }
    Worker::GetInstance().Clear();
    Server::Instance().Clear();
  }

  void StartAddServable() {
    auto status = StartServable(servable_dir_, servable_name_, 0);
    ASSERT_TRUE(status.IsSuccess());
  }

  void RegisterAddServable(bool with_batch_dim = false) {
    DeclareServable(servable_name_, model_file_, "mindir", with_batch_dim);

    // register_method
    RegisterMethod(servable_name_, "add_common", {"x1", "x2"}, {"y"}, 2, 1);
  }

  static Status StartServable(const std::string &servable_dir, const std::string &servable_name, int version_number) {
    auto notify_master = std::make_shared<LocalNotifyMaster>();
    ServableContext::Instance()->SetDeviceId(0);
    ServableContext::Instance()->SetDeviceTypeStr("Ascend");
    auto servable = std::make_shared<LocalModelServable>();
    auto status = servable->StartServable(servable_dir, servable_name, version_number);
    if (status != SUCCESS) {
      return status;
    }
    status = Worker::GetInstance().StartServable(servable, notify_master);
    return status;
  }
  static void DeclareServable(const std::string &servable_name, const std::string &servable_file,
                              const std::string &model_type, bool with_batch_dim = false) {
    ServableMeta servable_meta;
    servable_meta.common_meta.servable_name = servable_name;
    servable_meta.common_meta.with_batch_dim = with_batch_dim;
    servable_meta.local_meta.servable_file = servable_file;
    servable_meta.local_meta.SetModelFormat(model_type);
    // declare_servable
    ServableStorage::Instance().DeclareServable(servable_meta);
  }
  static Status RegisterMethod(const std::string &servable_name, const std::string &method_name,
                               const std::vector<std::string> &input_names,
                               const std::vector<std::string> &output_names, size_t servable_input_count,
                               size_t servable_output_count) {
    auto status =
      ServableStorage::Instance().RegisterInputOutputInfo(servable_name, servable_input_count, servable_output_count);
    if (status != SUCCESS) {
      return status;
    }

    MethodSignature method_signature;
    method_signature.servable_name = servable_name;
    method_signature.method_name = method_name;
    method_signature.inputs = input_names;
    method_signature.outputs = output_names;
    // method input 0 and input 1 as servable input
    method_signature.servable_inputs = {{kPredictPhaseTag_Input, 0}, {kPredictPhaseTag_Input, 1}};
    // servable output as method output
    method_signature.returns = {{kPredictPhaseTag_Predict, 0}};
    ServableStorage::Instance().RegisterMethod(method_signature);
    return SUCCESS;
  }
  std::string servable_dir_;
  std::string servable_name_;
  int version_number_ = 0;
  std::string model_file_;
  std::string model_name_path_;
  std::string version_number_path_;
  std::string servable_name_path_;
  std::set<std::string> servable_dir_list_;
  std::set<std::string> model_name_path_list_;
  std::set<std::string> version_number_path_list_;
  std::set<std::string> servable_name_path_list_;
};

class TestMasterWorkerClient : public TestMasterWorker {
 public:
  TestMasterWorkerClient() = default;

  static void InitTensor(proto::Tensor *tensor, const std::vector<int64_t> &shape, proto::DataType data_type,
                         const void *data, size_t data_size) {
    MSI_EXCEPTION_IF_NULL(tensor);
    tensor->set_dtype(data_type);
    auto proto_shape = tensor->mutable_shape();
    for (auto item : shape) {
      proto_shape->add_dims(item);
    }
    tensor->set_data(data, data_size);
  }

  static std::vector<float> InitOneInstanceRequest(proto::PredictRequest *request, const std::string &servable_name,
                                                   const std::string &method_name, int version_number) {
    MSI_EXCEPTION_IF_NULL(request);
    auto request_servable_spec = request->mutable_servable_spec();
    request_servable_spec->set_name(servable_name);
    request_servable_spec->set_method_name(method_name);
    request_servable_spec->set_version_number(version_number);

    std::vector<float> x1_data = {1.1, 2.2, 3.3, 4.4};
    std::vector<float> x2_data = {1.2, 2.3, 3.4, 4.5};
    std::vector<float> y_data;
    for (size_t i = 0; i < x1_data.size(); i++) {
      y_data.push_back(x1_data[i] + x2_data[i]);
    }
    auto instance = request->add_instances();
    auto &input_map = (*instance->mutable_items());
    // input x1
    InitTensor(&input_map["x1"], {2, 2}, proto::MS_FLOAT32, x1_data.data(), x1_data.size() * sizeof(float));
    // input x2
    InitTensor(&input_map["x2"], {2, 2}, proto::MS_FLOAT32, x2_data.data(), x2_data.size() * sizeof(float));
    return y_data;
  }
  template <class IN_DT = float, class OUT_DT = float>
  static std::vector<std::vector<OUT_DT>> InitMultiInstancesRequest(proto::PredictRequest *request,
                                                                    const std::string &servable_name,
                                                                    const std::string &method_name, int version_number,
                                                                    size_t instances_count) {
    MSI_EXCEPTION_IF_NULL(request);
    auto request_servable_spec = request->mutable_servable_spec();
    request_servable_spec->set_name(servable_name);
    request_servable_spec->set_method_name(method_name);
    request_servable_spec->set_version_number(version_number);

    auto data_type = proto::MS_FLOAT32;
    if (std::string(typeid(IN_DT).name()) == std::string(typeid(int32_t).name())) {
      data_type = proto::MS_INT32;
    }

    std::vector<std::vector<OUT_DT>> y_data_list;
    for (size_t k = 0; k < instances_count; k++) {
      std::vector<float> x1_data_org = {1.1, 2.2, 3.3, 4.4};
      std::vector<float> x2_data_org = {6.6, 7.7, 8.8, 9.9};

      std::vector<IN_DT> x1_data;
      std::vector<IN_DT> x2_data;

      std::vector<OUT_DT> y_data;
      for (size_t i = 0; i < x1_data_org.size(); i++) {
        x1_data.push_back(static_cast<IN_DT>(x1_data_org[i] * (k + 1)));
        x2_data.push_back(static_cast<IN_DT>(x2_data_org[i] * (k + 1)));
        y_data.push_back(static_cast<OUT_DT>(x1_data[i] + x2_data[i]));
      }
      y_data_list.push_back(y_data);

      auto instance = request->add_instances();
      auto &input_map = (*instance->mutable_items());
      // input x1
      InitTensor(&input_map["x1"], {2, 2}, data_type, x1_data.data(), x1_data.size() * sizeof(IN_DT));
      // input x2
      InitTensor(&input_map["x2"], {2, 2}, data_type, x2_data.data(), x2_data.size() * sizeof(IN_DT));
    }
    return y_data_list;
  }

  template <class IN_DT = float, class OUT_DT = float>
  static std::vector<std::vector<OUT_DT>> InitMultiInstancesShape2Request(proto::PredictRequest *request,
                                                                          const std::string &servable_name,
                                                                          const std::string &method_name,
                                                                          int version_number, size_t instances_count) {
    MSI_EXCEPTION_IF_NULL(request);
    auto request_servable_spec = request->mutable_servable_spec();
    request_servable_spec->set_name(servable_name);
    request_servable_spec->set_method_name(method_name);
    request_servable_spec->set_version_number(version_number);

    auto data_type = proto::MS_FLOAT32;
    if (std::string(typeid(IN_DT).name()) == std::string(typeid(int32_t).name())) {
      data_type = proto::MS_INT32;
    }

    std::vector<std::vector<OUT_DT>> y_data_list;
    for (size_t k = 0; k < instances_count; k++) {
      std::vector<float> x1_data_org = {1.1, 2.2};
      std::vector<float> x2_data_org = {8.8, 9.9};

      std::vector<IN_DT> x1_data;
      std::vector<IN_DT> x2_data;

      std::vector<OUT_DT> y_data;
      for (size_t i = 0; i < x1_data_org.size(); i++) {
        x1_data.push_back(static_cast<IN_DT>(x1_data_org[i] * (k + 1)));
        x2_data.push_back(static_cast<IN_DT>(x2_data_org[i] * (k + 1)));
        y_data.push_back(x1_data[i] + x2_data[i]);
      }
      y_data_list.push_back(y_data);

      auto instance = request->add_instances();
      auto &input_map = (*instance->mutable_items());
      // input x1
      InitTensor(&input_map["x1"], {2}, data_type, x1_data.data(), x1_data.size() * sizeof(IN_DT));
      // input x2
      InitTensor(&input_map["x2"], {2}, data_type, x2_data.data(), x2_data.size() * sizeof(IN_DT));
    }
    return y_data_list;
  }

  template <class OUT_DT>
  static void CheckMultiInstanceResult(const proto::PredictReply &reply,
                                       const std::vector<std::vector<OUT_DT>> &y_data_list,
                                       size_t instances_count) {  // checkout output
    ASSERT_EQ(reply.instances_size(), instances_count);
    ASSERT_EQ(reply.error_msg_size(), 0);
    auto data_type = proto::MS_FLOAT32;
    if (std::string(typeid(OUT_DT).name()) == std::string(typeid(int32_t).name())) {
      data_type = proto::MS_INT32;
    }
    std::vector<int64_t> shape;
    if (y_data_list[0].size() == 4) {
      shape = {2, 2};
    } else {
      shape = {2};
    }
    for (size_t k = 0; k < instances_count; k++) {
      auto &output_instance = reply.instances(k);
      ASSERT_EQ(output_instance.items_size(), 1);
      auto &output_items = output_instance.items();
      ASSERT_EQ(output_items.begin()->first, "y");
      auto &output_tensor = output_items.begin()->second;

      CheckTensor(output_tensor, shape, data_type, y_data_list[k].data(), y_data_list[k].size() * sizeof(OUT_DT));
    }
  }

  template <class OUT_DT>
  static void CheckInstanceResult(const proto::PredictReply &reply, const std::vector<OUT_DT> &y_data) {
    // checkout output
    ASSERT_EQ(reply.instances_size(), 1);
    ASSERT_EQ(reply.error_msg_size(), 0);
    auto data_type = proto::MS_FLOAT32;
    if (std::string(typeid(OUT_DT).name()) == std::string(typeid(int32_t).name())) {
      data_type = proto::MS_INT32;
    }
    std::vector<int64_t> shape;
    if (y_data.size() == 4) {
      shape = {2, 2};
    } else {
      shape = {2};
    }
    auto &output_instance = reply.instances(0);
    ASSERT_EQ(output_instance.items_size(), 1);
    auto &output_items = output_instance.items();
    ASSERT_EQ(output_items.begin()->first, "y");
    auto &output_tensor = output_items.begin()->second;

    CheckTensor(output_tensor, shape, data_type, y_data.data(), y_data.size() * sizeof(OUT_DT));
  }

  static void CheckTensor(const proto::Tensor &output_tensor, const std::vector<int64_t> &shape,
                          proto::DataType data_type, const void *data, size_t data_size) {
    EXPECT_EQ(output_tensor.dtype(), data_type);
    // check shape [2,2]
    auto &output_tensor_shape = output_tensor.shape();
    ASSERT_EQ(output_tensor_shape.dims_size(), shape.size());
    std::vector<int64_t> proto_shape;
    for (size_t i = 0; i < output_tensor_shape.dims_size(); i++) {
      proto_shape.push_back(output_tensor_shape.dims(i));
    }
    EXPECT_EQ(proto_shape, shape);

    // check data
    ASSERT_EQ(output_tensor.data().size(), data_size);
    switch (data_type) {
      case proto::MS_FLOAT32: {
        auto data_len = data_size / sizeof(float);
        auto real_data = reinterpret_cast<const float *>(output_tensor.data().data());
        auto expect_data = reinterpret_cast<const float *>(data);
        for (size_t i = 0; i < data_len; i++) {
          EXPECT_EQ(real_data[i], expect_data[i]);
          if (real_data[i] != expect_data[i]) {
            break;
          }
        }
        break;
      }
      case proto::MS_INT32: {
        auto data_len = data_size / sizeof(int32_t);
        auto real_data = reinterpret_cast<const int32_t *>(output_tensor.data().data());
        auto expect_data = reinterpret_cast<const int32_t *>(data);
        for (size_t i = 0; i < data_len; i++) {
          EXPECT_EQ(real_data[i], expect_data[i]);
          if (real_data[i] != expect_data[i]) {
            break;
          }
        }
        break;
      }
      default:
        FAIL();
    }
  }
  static grpc::Status Dispatch(const proto::PredictRequest &request, proto::PredictReply *reply) {
    MSServiceImpl impl(Server::Instance().GetDispatcher());
    grpc::ServerContext context;
    auto promise = std::make_shared<std::promise<void>>();
    auto future = promise->get_future();
    PredictOnFinish callback = [promise]() { promise->set_value(); };
    impl.PredictAsync(&request, reply, callback);
    future.get();
    return grpc::Status::OK;
  }
};

}  // namespace serving
}  // namespace mindspore
#endif  // MINDSPORE_SERVING_TEST_SERVABLE_COMMON_H
