/* Copyright 2019 The TensorFlow 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.
==============================================================================*/
#include "tensorflow/lite/experimental/delegates/coreml/builders/convolution_op_builder.h"

#include "google/protobuf/repeated_field.h"
#include "external/coremltools/mlmodel/format/NeuralNetwork.pb.h"
#include "tensorflow/lite/c/common.h"
#include "tensorflow/lite/experimental/delegates/coreml/builders/activation_layer_builder.h"
#include "tensorflow/lite/experimental/delegates/coreml/builders/op_factory.h"
#include "tensorflow/lite/experimental/delegates/coreml/builders/op_validator.h"
#include "tensorflow/lite/kernels/internal/optimized/optimized_ops.h"
#include "tensorflow/lite/kernels/kernel_util.h"

namespace tflite {
namespace delegates {
namespace coreml {
const char* ConvolutionOpBuilder::DebugName() {
  if (!str_debug_name_[0])
    GetDebugName("ConvolutionOpBuilder", node_id_, str_debug_name_);
  return str_debug_name_;
}

void ConvolutionOpBuilder::SetWeights(TfLiteTensor* weights) {
  weights_ = weights;
}

void ConvolutionOpBuilder::SetBias(TfLiteTensor* bias) { bias_ = bias; }

void ConvolutionOpBuilder::SetOutputShape(TfLiteTensor* output_shape) {
  output_shape_ = output_shape;
}

CoreML::Specification::NeuralNetworkLayer* ConvolutionOpBuilder::Build() {
  if (layer_ == nullptr) {
    layer_.reset(new CoreML::Specification::NeuralNetworkLayer);
  }
  layer_->set_name(DebugName());

  int stride_height = 0;
  int stride_width = 0;
  int dilation_height = 0;
  int dilation_width = 0;
  TfLitePadding padding;

  switch (conv_type_) {
    case ConvolutionType::kConv: {
      const auto* conv_params =
          reinterpret_cast<const TfLiteConvParams*>(builtin_data_);
      stride_height = conv_params->stride_height;
      stride_width = conv_params->stride_width;
      dilation_height = conv_params->dilation_height_factor;
      dilation_width = conv_params->dilation_width_factor;
      padding = conv_params->padding;

      layer_->mutable_convolution()->set_ngroups(1);
      break;
    }
    case ConvolutionType::kDepthwiseConv: {
      const auto* depthwise_conv_params =
          reinterpret_cast<const TfLiteDepthwiseConvParams*>(builtin_data_);
      stride_height = depthwise_conv_params->stride_height;
      stride_width = depthwise_conv_params->stride_width;
      dilation_height = depthwise_conv_params->dilation_height_factor;
      dilation_width = depthwise_conv_params->dilation_width_factor;
      padding = depthwise_conv_params->padding;

      // n_groups = kernel_channel / depth_multiplier
      layer_->mutable_convolution()->set_ngroups(
          weights_->dims->data[3] / depthwise_conv_params->depth_multiplier);
      break;
    }
    case ConvolutionType::kTransposeConv: {
      const auto* transpose_conv_params =
          reinterpret_cast<const TfLiteTransposeConvParams*>(builtin_data_);
      const int height_index = 1;
      const int width_index = 2;

      stride_height = transpose_conv_params->stride_height;
      stride_width = transpose_conv_params->stride_width;
      padding = transpose_conv_params->padding;

      layer_->mutable_convolution()->mutable_outputshape()->Add(
          GetTensorData<int32_t>(output_shape_)[height_index]);
      layer_->mutable_convolution()->mutable_outputshape()->Add(
          GetTensorData<int32_t>(output_shape_)[width_index]);
      break;
    }
  }

  // If not set, it will default to (1,1)
  if (stride_height) {
    layer_->mutable_convolution()->add_stride(stride_height);
    layer_->mutable_convolution()->add_stride(stride_width);
  }

  if (dilation_height) {
    layer_->mutable_convolution()->add_dilationfactor(dilation_height);
    layer_->mutable_convolution()->add_dilationfactor(dilation_width);
  }

  switch (padding) {
    case kTfLitePaddingSame:
      layer_->mutable_convolution()->mutable_same();
      break;
    case kTfLitePaddingValid:
      layer_->mutable_convolution()->mutable_valid();
      break;
    case kTfLitePaddingUnknown:
      fprintf(stderr, "Padding is unknown.\n");
      break;
  }

  FillCoreMLWeights();
  FillCoreMLBias();

  return layer_.release();
}

void ConvolutionOpBuilder::FillCoreMLWeights() {
  if (conv_type_ == ConvolutionType::kDepthwiseConv) {
    layer_->mutable_convolution()->set_kernelchannels(1);
    layer_->mutable_convolution()->set_outputchannels(weights_->dims->data[3]);
  } else {
    layer_->mutable_convolution()->set_kernelchannels(weights_->dims->data[3]);
    layer_->mutable_convolution()->set_outputchannels(weights_->dims->data[0]);
  }
  layer_->mutable_convolution()->add_kernelsize(weights_->dims->data[1]);
  layer_->mutable_convolution()->add_kernelsize(weights_->dims->data[2]);

  TransposeKernelWeights();  // Should be called after CoreML shape is set.
}

void ConvolutionOpBuilder::TransposeKernelWeights() {
  RuntimeShape tfl_shape(4, weights_->dims->data);
  // CoreML kernel has shape of (C_out, C_in, H, W)
  RuntimeShape coreml_shape(
      {static_cast<int>(layer_->convolution().outputchannels()),
       static_cast<int>(layer_->convolution().kernelchannels()),
       static_cast<int>(layer_->convolution().kernelsize()[0]),
       static_cast<int>(layer_->convolution().kernelsize()[1])});

  TransposeParams params;

  if (conv_type_ == ConvolutionType::kDepthwiseConv) {
    // DepthwiseConv2D: TFL kernel has shape of (1, H, W, C_out),
    // and CoreML kernel has shape of (C_out, 1, H, W)
    params = {/*perm_count=*/4, /*perm=*/{3, 0, 1, 2}};
  } else {
    // Conv2D and TransposeConv: TFL kernel has shape of (C_out, H, W, C_in),
    // and CoreML kernel has shape of (C_out, C_in, H, W)
    params = {/*perm_count=*/4, /*perm=*/{0, 3, 1, 2}};
  }

  if (conv_type_ == ConvolutionType::kTransposeConv) {
    layer_->mutable_convolution()->set_isdeconvolution(true);
  }

  if (weights_->type == kTfLiteFloat32) {
    auto* coreml_weights =
        layer_->mutable_convolution()->mutable_weights()->mutable_floatvalue();
    coreml_weights->Resize(NumElements(weights_), 0);

    optimized_ops::Transpose<float>(params, tfl_shape, weights_->data.f,
                                    coreml_shape,
                                    coreml_weights->mutable_data());
  } else if (weights_->type == kTfLiteFloat16) {
    auto* coreml_weights = layer_->mutable_convolution()
                               ->mutable_weights()
                               ->mutable_float16value();
    // float16value has type of bytes (std::string)
    coreml_weights->resize(weights_->bytes, 0);

    optimized_ops::Transpose<uint16_t>(
        params, tfl_shape, reinterpret_cast<uint16_t*>(weights_->data.raw),
        coreml_shape, reinterpret_cast<uint16_t*>(&coreml_weights->front()));
  }
}

void ConvolutionOpBuilder::FillCoreMLBias() {
  if (bias_ != nullptr) {
    layer_->mutable_convolution()->set_hasbias(true);
    if (bias_->type == kTfLiteFloat32) {
      std::copy(bias_->data.f, bias_->data.f + NumElements(bias_->dims),
                google::protobuf::RepeatedFieldBackInserter(layer_->mutable_convolution()
                                                      ->mutable_bias()
                                                      ->mutable_floatvalue()));
    } else if (bias_->type == kTfLiteFloat16) {
      // float16value has type of bytes (std::string)
      layer_->mutable_convolution()
          ->mutable_bias()
          ->mutable_float16value()
          ->assign(bias_->data.raw, bias_->bytes);
    }
  }
}

TfLiteStatus ConvolutionOpBuilder::PopulateSubgraph(TfLiteContext* context) {
  TfLiteFusedActivation activation;
  switch (conv_type_) {
    case ConvolutionType::kConv: {
      const auto* conv_params =
          reinterpret_cast<const TfLiteConvParams*>(builtin_data_);
      activation = conv_params->activation;
      break;
    }
    case ConvolutionType::kDepthwiseConv: {
      const auto* depthwise_conv_params =
          reinterpret_cast<const TfLiteDepthwiseConvParams*>(builtin_data_);
      activation = depthwise_conv_params->activation;
      break;
    }
    case ConvolutionType::kTransposeConv: {
      activation = kTfLiteActNone;
      break;
    }
  }

  if (activation == kTfLiteActNone) {
    builder_output_ = AddOutput();
  } else {
    ActivationLayerBuilder* activation_builder =
        reinterpret_cast<ActivationLayerBuilder*>(
            graph_builder_->AddBuilder(CreateActivationLayerBuilder, nullptr));
    activation_builder->SetActivation(activation);
    activation_builder->AddInput(AddOutput());
    activation_builder->PopulateSubgraph(context);
    builder_output_ = activation_builder->GetOutput(context);
  }
  return kTfLiteOk;
}

TfLiteStatus ConvolutionOpBuilder::RegisterInputs(const TfLiteIntArray* inputs,
                                                  TfLiteContext* context) {
  if (conv_type_ == ConvolutionType::kTransposeConv) {
    if (inputs->size != 3) {
      TF_LITE_KERNEL_LOG(context,
                         "Transpose Conv should have 3 inputs, %d given.",
                         inputs->size);
      return kTfLiteError;
    }
    AddInput(inputs->data[2]);
    SetOutputShape(&context->tensors[inputs->data[0]]);
  } else {
    if (inputs->size != 2 && inputs->size != 3) {
      TF_LITE_KERNEL_LOG(context,
                         "Convolution and depthwise convolution should have 2 "
                         "or 3 inputs, %d given.",
                         inputs->size);
      return kTfLiteError;
    }
    AddInput(inputs->data[0]);
    if (inputs->size > 2) {
      SetBias(&context->tensors[inputs->data[2]]);
    }
  }
  SetWeights(&context->tensors[inputs->data[1]]);
  return kTfLiteOk;
}

TfLiteStatus ConvolutionOpBuilder::RegisterOutputs(
    const TfLiteIntArray* outputs, TfLiteContext* context) {
  if (outputs->size != 1) {
    TF_LITE_KERNEL_LOG(context, "Wrong # of outputs!.");
    return kTfLiteError;
  }
  TensorID output_tensor = GetOutput(context);
  if (output_tensor.NodeID() == -1) {
    TF_LITE_KERNEL_LOG(context, "Failed to build output tensor.");
    return kTfLiteError;
  }
  graph_builder_->AddTensorWithID(outputs->data[0], output_tensor);
  return kTfLiteOk;
}

OpBuilder* CreateConvolutionOpBuilder(GraphBuilder* graph_builder) {
  return new ConvolutionOpBuilder(graph_builder, ConvolutionType::kConv);
}

OpBuilder* CreateDepthwiseConvolutionOpBuilder(GraphBuilder* graph_builder) {
  return new ConvolutionOpBuilder(graph_builder,
                                  ConvolutionType::kDepthwiseConv);
}

OpBuilder* CreateTransposeConvolutionOpBuilder(GraphBuilder* graph_builder) {
  return new ConvolutionOpBuilder(graph_builder,
                                  ConvolutionType::kTransposeConv);
}

bool IsConvolutionOpSupported(const TfLiteRegistration* registration,
                              const TfLiteNode* node, TfLiteContext* context) {
  if (node->builtin_data == nullptr) return false;

  TfLiteFusedActivation activation;

  if (registration->builtin_code == kTfLiteBuiltinConv2d) {
    const auto* conv_params =
        reinterpret_cast<const TfLiteConvParams*>(node->builtin_data);
    activation = conv_params->activation;
  } else if (registration->builtin_code == kTfLiteBuiltinDepthwiseConv2d) {
    const auto* depthwise_conv_params =
        reinterpret_cast<const TfLiteDepthwiseConvParams*>(node->builtin_data);
    activation = depthwise_conv_params->activation;
  } else if (registration->builtin_code == kTfLiteBuiltinTransposeConv) {
    activation = kTfLiteActNone;
  } else {
    TF_LITE_KERNEL_LOG(
        context,
        "Invalid op: op must be Conv2D, DepthwiseConv2D or TransposeConv.");
    return false;
  }

  if (activation == kTfLiteActSignBit) {
    return false;
  }

  const int kOutputShapeTensor = 0;  // Only used for TransposeConv
  const int kWeightTensor = 1;
  const int kBiasTensor = 2;  // Only used for non-TransposeConv
  const TfLiteTensor* weights;
  TF_LITE_ENSURE_OK(context,
                    GetInputSafe(context, node, kWeightTensor, &weights));
  const int max_kernel_size = 16384;
  if (!IsConstantTensor(weights)) {
    return false;
  }
  if (weights->dims->data[1] > max_kernel_size ||
      weights->dims->data[2] > max_kernel_size) {
    return false;
  }
  if (registration->builtin_code == kTfLiteBuiltinTransposeConv) {
    if (!IsConstantTensor(GetInput(context, node, kOutputShapeTensor))) {
      return false;
    }
  } else {
    if (node->inputs->size >= kBiasTensor &&
        !IsConstantTensor(GetInput(context, node, kBiasTensor))) {
      return false;
    }
  }

  return true;
}

bool IsDepthwiseConvolutionOpSupported(const TfLiteRegistration* registration,
                                       const TfLiteNode* node,
                                       TfLiteContext* context) {
  return IsConvolutionOpSupported(registration, node, context);
}

bool IsTransposeConvolutionOpSupported(const TfLiteRegistration* registration,
                                       const TfLiteNode* node,
                                       TfLiteContext* context) {
  return IsConvolutionOpSupported(registration, node, context);
}

}  // namespace coreml
}  // namespace delegates
}  // namespace tflite
