#include "av2/encoder/intra_dip_mode_prune_tflite.h"

#include <cstdio>
#include <memory>
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <mutex>

#include "common/tf_lite_includes.h"

#if HAVE_FEXCEPT
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <fenv.h>
#endif

struct DipContext {
  std::unique_ptr<tflite::Interpreter> interpreter;
  std::unique_ptr<tflite::FlatBufferModel> model;
  dip_pruning_inputs dip_pruning_in;
  int model_index = -1;
};

std::mutex dip_prune_mutex;

static void create_interpreter(DipContext *context,
                               const unsigned char *model_def, int model_len) {
  std::lock_guard<std::mutex> lock(dip_prune_mutex);
  tflite::LoggerOptions::SetMinimumLogSeverity(tflite::TFLITE_LOG_ERROR);
  std::unique_ptr<tflite::FlatBufferModel> model =
      tflite::FlatBufferModel::BuildFromBuffer((const char *)model_def,
                                               model_len);
  tflite::ops::builtin::BuiltinOpResolver resolver;
  std::unique_ptr<tflite::Interpreter> interpreter;
  if (tflite::InterpreterBuilder(*model, resolver)(&interpreter) != kTfLiteOk) {
    std::cerr << "Failed to build interpreter for DIP model." << std::endl;
    exit(1);
  }

  if (interpreter->AllocateTensors() != kTfLiteOk) {
    std::cerr << "Failed to allocate tensors for DIP model." << std::endl;
    exit(1);
  }

  context->interpreter = std::move(interpreter);
  context->model = std::move(model);
}

static void ensure_tflite_init(void **context, int model_index) {
  if (*context == nullptr) {
    *context = new DipContext();
  }
  DipContext *ctx = (DipContext *)*context;
  if (!ctx->interpreter || model_index != ctx->model_index) {
    ctx->model_index = model_index;
    const uint8_t *model_bytes = NULL;
    int model_len = -1;

    switch (model_index) {
      case 0:
        model_bytes = dip_pruning_tflite_qp85;
        model_len = sizeof(dip_pruning_tflite_qp85) /
                    sizeof(dip_pruning_tflite_qp85[0]);
        break;
      case 1:
        model_bytes = dip_pruning_tflite_qp110;
        model_len = sizeof(dip_pruning_tflite_qp110) /
                    sizeof(dip_pruning_tflite_qp110[0]);
        break;
      case 2:
        model_bytes = dip_pruning_tflite_qp135;
        model_len = sizeof(dip_pruning_tflite_qp135) /
                    sizeof(dip_pruning_tflite_qp135[0]);
        break;
      case 3:
        model_bytes = dip_pruning_tflite_qp160;
        model_len = sizeof(dip_pruning_tflite_qp160) /
                    sizeof(dip_pruning_tflite_qp160[0]);
        break;
      case 4:
        model_bytes = dip_pruning_tflite_qp185;
        model_len = sizeof(dip_pruning_tflite_qp185) /
                    sizeof(dip_pruning_tflite_qp185[0]);
        break;
      case 5:
        model_bytes = dip_pruning_tflite_qp210;
        model_len = sizeof(dip_pruning_tflite_qp210) /
                    sizeof(dip_pruning_tflite_qp210[0]);
        break;
      default:
        fprintf(stderr, "Bad DIP pruning model index: %d\n", model_index);
        exit(1);
    }

    create_interpreter(ctx, (const unsigned char *)model_bytes, model_len);

    ctx->dip_pruning_in.inputs[0].name = "extra_features";
    ctx->dip_pruning_in.inputs[0].size = 19;
    ctx->dip_pruning_in.inputs[0].orig_index = 0;
    ctx->dip_pruning_in.inputs[0].tflite_index = 3;
    ctx->dip_pruning_in.inputs[1].name = "source_pixels";
    ctx->dip_pruning_in.inputs[1].size = 64;
    ctx->dip_pruning_in.inputs[1].orig_index = 1;
    ctx->dip_pruning_in.inputs[1].tflite_index = 0;
    ctx->dip_pruning_in.inputs[2].name = "dip_features";
    ctx->dip_pruning_in.inputs[2].size = 11;
    ctx->dip_pruning_in.inputs[2].orig_index = 2;
    ctx->dip_pruning_in.inputs[2].tflite_index = 1;
    ctx->dip_pruning_in.inputs[3].name = "block_size";
    ctx->dip_pruning_in.inputs[3].size = 2;
    ctx->dip_pruning_in.inputs[3].orig_index = 3;
    ctx->dip_pruning_in.inputs[3].tflite_index = 4;
    ctx->dip_pruning_in.inputs[4].name = "dip_model_rds";
    ctx->dip_pruning_in.inputs[4].size = 12;
    ctx->dip_pruning_in.inputs[4].orig_index = 4;
    ctx->dip_pruning_in.inputs[4].tflite_index = 2;
  }
}

#if HAVE_FEXCEPT && CONFIG_DEBUG
#define FLOATING_POINT_DISABLE_EXCEPTIONS \
  const int float_excepts = fedisableexcept(FE_UNDERFLOW | FE_OVERFLOW);
#define FLOATING_POINT_RESTORE_EXCEPTIONS feenableexcept(float_excepts);
#else
#define FLOATING_POINT_DISABLE_EXCEPTIONS
#define FLOATING_POINT_RESTORE_EXCEPTIONS
#endif  // HAVE_FEXCEPT && CONFIG_DEBUG

static std::vector<float> run_inference(void **context) {
  DipContext *ctx = (DipContext *)*context;
  tflite::Interpreter *interpreter = ctx->interpreter.get();
  for (int i = 0; i < DIP_PRUNING_NUM_INPUTS; i++) {
    int tflite_index = ctx->dip_pruning_in.inputs[i].tflite_index;
    float *tensor_input = interpreter->typed_input_tensor<float>(tflite_index);
    memcpy(tensor_input, ctx->dip_pruning_in.inputs[i].values,
           ctx->dip_pruning_in.inputs[i].size * sizeof(float));
  }

  FLOATING_POINT_DISABLE_EXCEPTIONS
  if (interpreter->Invoke() != kTfLiteOk) {
    std::cerr << "Failed to run DIP pruning inference." << std::endl;
    exit(1);
  }
  FLOATING_POINT_RESTORE_EXCEPTIONS

  float *output = interpreter->typed_output_tensor<float>(0);
  size_t output_size =
      interpreter->tensor(interpreter->outputs()[0])->bytes / sizeof(float);
  std::vector<float> output_data(output, output + output_size);

  return output_data;
}

extern "C" int intra_dip_mode_prune_tflite(void **context, float *output,
                                           int qp) {
  ensure_tflite_init(context, intra_dip_mode_prune_get_model_index(qp));
  auto output_vec = run_inference(context);
  memcpy(output, output_vec.data(), 1 * sizeof(float));
  return 0;
}

extern "C" int intra_dip_mode_prune_get_model_index(int qp) {
  int closest_index = 0;
  int closest_delta = 10000;
  for (int i = 0; i < 6; i++) {
    int delta = abs(qp - DIP_PRUNING_QPS[i]);
    if (delta < closest_delta) {
      closest_delta = delta;
      closest_index = i;
    }
  }
  // Don't run pruning for QP>=210 (index 5).
  // TODO(comc): Re-train QP=210 model.
  if (closest_index == 5) {
    return -1;
  }
  return closest_index;
}

extern "C" dip_pruning_inputs *intra_dip_mode_prune_get_inputs(void **context,
                                                               int qp) {
  ensure_tflite_init(context, intra_dip_mode_prune_get_model_index(qp));
  DipContext *ctx = (DipContext *)*context;
  return &(ctx->dip_pruning_in);
}

extern "C" void intra_dip_mode_prune_normalize_and_resize_8x8(
    const uint16_t *input, size_t stride, int bd, size_t width, size_t height,
    float *output) {
  const size_t x_step = width / 8;
  const size_t y_step = height / 8;
  const float norm = (float)((1 << bd) - 1);
  for (size_t out_y = 0; out_y < 8; out_y++) {
    const size_t in_y = out_y * y_step;
    for (size_t out_x = 0; out_x < 8; out_x++) {
      const size_t in_x = out_x * x_step;
      const size_t in_index = in_y * stride + in_x;
      const size_t out_index = out_y * 8 + out_x;
      output[out_index] = (float)input[in_index] / norm;
    }
  }
}

extern "C" void intra_dip_mode_prune_close(void **context) {
  DipContext *ctx = (DipContext *)*context;
  if (ctx != nullptr) delete ctx;
  *context = nullptr;
}
