/* 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/delegates/gpu/gl/runtime.h"

#include <algorithm>
#include <cstdint>
#include <vector>

#include "absl/strings/str_cat.h"
#include "tensorflow/lite/delegates/gpu/common/data_type.h"
#include "tensorflow/lite/delegates/gpu/common/gpu_info.h"
#include "tensorflow/lite/delegates/gpu/common/memory_management.h"
#include "tensorflow/lite/delegates/gpu/common/memory_management/types.h"
#include "tensorflow/lite/delegates/gpu/common/status.h"
#include "tensorflow/lite/delegates/gpu/common/types.h"
#include "tensorflow/lite/delegates/gpu/gl/gl_call.h"
#include "tensorflow/lite/delegates/gpu/gl/gl_errors.h"
#include "tensorflow/lite/delegates/gpu/gl/gl_program.h"
#include "tensorflow/lite/delegates/gpu/gl/gl_texture.h"
#include "tensorflow/lite/delegates/gpu/gl/object.h"
#include "tensorflow/lite/delegates/gpu/gl/portable_gl31.h"
#include "tensorflow/lite/delegates/gpu/gl/variable.h"

namespace tflite {
namespace gpu {
namespace gl {
namespace {

struct TextureF16Maker {
  absl::Status operator()(const uint3& size) const {
    return CreateReadOnlyImageTextureF16(size, data, gl_texture);
  }
  absl::Status operator()(const uint2& size) const {
    return CreateReadOnlyImageTextureF16(size, data, gl_texture);
  }
  absl::Status operator()(const size_t& size) const {
    return CreateReadOnlyImageTextureF16(uint2(static_cast<uint32_t>(size), 1U),
                                         data, gl_texture);
  }
  absl::Span<const uint16_t> data;
  GlTexture* gl_texture;
};

struct TextureF32Maker {
  absl::Status operator()(const uint3& size) const {
    return CreateReadOnlyImageTexture(size, data, gl_texture);
  }
  absl::Status operator()(const uint2& size) const {
    return CreateReadOnlyImageTexture(size, data, gl_texture);
  }
  absl::Status operator()(const size_t& size) const {
    return CreateReadOnlyImageTexture(uint2(static_cast<uint32_t>(size), 1U),
                                      data, gl_texture);
  }
  absl::Span<const float> data;
  GlTexture* gl_texture;
};

absl::Status MakeGlTexture(const Object& object, const ObjectData& data,
                           GlTexture* gl_texture) {
  if (object.access == AccessType::READ_WRITE ||
      object.access == AccessType::WRITE) {
    return absl::InvalidArgumentError("Read-write textures are not supported");
  }
  if (object.data_type != DataType::FLOAT16 &&
      object.data_type != DataType::FLOAT32) {
    return absl::InvalidArgumentError(
        "Textures support float16 or float32 only.");
  }
  switch (object.data_type) {
    case DataType::FLOAT16: {
      if (data.size() % 2 != 0) {
        return absl::InvalidArgumentError("Texture size is not aligned");
      }
      return absl::visit(
          TextureF16Maker{
              .data = absl::MakeConstSpan(
                  reinterpret_cast<const uint16_t*>(data.data()),
                  data.size() / 2),
              .gl_texture = gl_texture,
          },
          object.size);
    }
    case DataType::FLOAT32: {
      if (data.size() % sizeof(float) != 0) {
        return absl::InvalidArgumentError("Texture size is not aligned");
      }
      return absl::visit(
          TextureF32Maker{
              .data = absl::MakeConstSpan(
                  reinterpret_cast<const float*>(data.data()),
                  data.size() / sizeof(float)),
              .gl_texture = gl_texture,
          },
          object.size);
    }
    default:
      return absl::InvalidArgumentError("Unsupported textures data type.");
  }
}

struct TextureRefMaker {
  absl::Status operator()(const uint3& size) const {
    return CreateReadWriteRgbaImageTexture(type, size, gl_texture);
  }
  absl::Status operator()(const uint2& size) const {
    return CreateReadWriteRgbaImageTexture(type, size, gl_texture);
  }
  absl::Status operator()(const size_t& size) const {
    return CreateReadWriteRgbaImageTexture(
        type, uint2(static_cast<uint32_t>(size), 1U), gl_texture);
  }
  DataType type;
  GlTexture* gl_texture;
};

// Makes read-write gl texture
absl::Status MakeGlTextureRef(const Object& object, GlTexture* gl_texture) {
  return absl::visit(TextureRefMaker{object.data_type, gl_texture},
                     object.size);
}

absl::Status MakeGlBuffer(const Object& object, const ObjectData& data,
                          GlBuffer* gl_buffer) {
  if (data.size() % SizeOf(object.data_type) != 0) {
    return absl::InvalidArgumentError("Buffer size is not aligned");
  }
  return CreateReadOnlyShaderStorageBuffer(absl::MakeConstSpan(data),
                                           gl_buffer);
}

// Looks up an object with the given id. If found, makes a binding function.
absl::Status MakeBindingFunc(const Object& object, uint32_t id,
                             const ObjectManager& objects,
                             std::function<absl::Status()>* binding_func) {
  const uint32_t binding = object.binding;
  switch (object.object_type) {
    case ObjectType::BUFFER: {
      auto ptr = objects.FindBuffer(id);
      if (!ptr) {
        return absl::NotFoundError(
            absl::StrCat("Buffer ", id, " is not found"));
      }

      // Validate buffer.
      size_t size_in_bytes = ByteSizeOf(object);
      // TODO(akulik): make comparison != instead of <
      if (ptr->bytes_size() < size_in_bytes) {
        return absl::FailedPreconditionError(
            absl::StrCat("Buffer ", id, " size in bytes ", ptr->bytes_size(),
                         " < requested size_in_bytes ", size_in_bytes));
      }
      *binding_func = [=]() { return ptr->BindToIndex(binding); };
      break;
    }
    case ObjectType::TEXTURE: {
      auto ptr = objects.FindTexture(id);
      if (!ptr) {
        return absl::NotFoundError(
            absl::StrCat("Texture ", id, " is not found"));
      }
      *binding_func = [=]() { return ptr->BindAsReadWriteImage(binding); };
      break;
    }
    case ObjectType::UNKNOWN:
      return absl::InvalidArgumentError("Unknown object type");
  }
  return absl::OkStatus();
}

}  // namespace

Runtime::Runtime(const RuntimeOptions& options, const GpuInfo& gpu_info,
                 CommandQueue* command_queue,
                 const ObjectManager* external_objects)
    : options_(options),
      gpu_info_(gpu_info),
      external_objects_(external_objects),
      command_queue_(command_queue) {
  programs_.reserve(256);
  if (options_.bundle_readonly_objects) {
    shared_readonly_buffer_ = absl::make_unique<SharedBufferData>();
  }
}

absl::Status Runtime::AddProgram(const GlShader& shader,
                                 const std::vector<Variable>& parameters,
                                 const std::vector<Object>& objects,
                                 const uint3& num_workgroups) {
  GlProgram program;
  RETURN_IF_ERROR(GlProgram::CreateWithShader(shader, &program));

  for (auto& parameter : parameters) {
    RETURN_IF_ERROR(program.SetParameter(parameter));
  }

  programs_.emplace_back(
      CompiledProgramDescriptor{std::move(program), num_workgroups, {}});

  // Create const buffers, resolve external references and collect internal
  // buffer references.
  for (auto& object : objects) {
    auto& program = programs_.back();
    BindFunc binding_func;
    if (IsRef(object)) {
      // Reference object could be provided externally as a model input/output
      // but also for debugging purposes. Otherwise all references are collected
      // and allocated later.
      absl::Status status = MakeBindingFunc(object, GetRef(object),
                                            *external_objects_, &binding_func);
      if (!status.ok()) {
        if (absl::IsNotFound(status)) {
          program.refs.push_back(object);
          continue;  // don't add to binding.
        }
        return status;
      }
    } else {
      // Allocate const object.
      uint32_t id;
      RETURN_IF_ERROR(AllocateConstObject(object, &id));
      RETURN_IF_ERROR(
          MakeBindingFunc(object, id, const_objects_, &binding_func));
    }
    program.bindings.push_back(std::move(binding_func));
  }

  // All parameters once set stay with program, therefore, we only need to keep
  // program and bindings for execution.
  return absl::OkStatus();
}

absl::Status Runtime::AllocateInternalObject(const Object& object) {
  const ObjectRef ref = GetRef(object);
  switch (object.object_type) {
    case ObjectType::BUFFER: {
      GlBuffer gl_buffer;
      RETURN_IF_ERROR(CreateReadWriteShaderStorageBuffer<uint8_t>(
          ByteSizeOf(object), &gl_buffer));
      RETURN_IF_ERROR(
          internal_objects_.RegisterBuffer(ref, std::move(gl_buffer)));
      break;
    }
    case ObjectType::TEXTURE: {
      GlTexture gl_texture;
      RETURN_IF_ERROR(MakeGlTextureRef(object, &gl_texture));
      RETURN_IF_ERROR(
          internal_objects_.RegisterTexture(ref, std::move(gl_texture)));
      break;
    }
    default:
      return absl::InternalError("Unexpected internal object type");
  }
  return absl::OkStatus();
}

absl::Status Runtime::AllocateConstObject(const Object& object, uint32_t* id) {
  const ObjectData* data = GetData(object);
  if (data == nullptr) {
    return absl::InternalError(
        "Unable to allocate reference as a const object");
  }
  *id = next_const_id_++;
  switch (object.object_type) {
    case ObjectType::BUFFER: {
      GlBuffer gl_buffer;
      if (!shared_readonly_buffer_ ||
          !shared_readonly_buffer_->Add(*data, &gl_buffer)) {
        RETURN_IF_ERROR(MakeGlBuffer(object, *data, &gl_buffer));
      }
      RETURN_IF_ERROR(const_objects_.RegisterBuffer(*id, std::move(gl_buffer)));
      break;
    }
    case ObjectType::TEXTURE: {
      GlTexture gl_texture;
      RETURN_IF_ERROR(MakeGlTexture(object, *data, &gl_texture));
      RETURN_IF_ERROR(
          const_objects_.RegisterTexture(*id, std::move(gl_texture)));
      break;
    }
    case ObjectType::UNKNOWN:
      return absl::InternalError("Unknown object type");
  }
  return absl::OkStatus();
}

absl::Status Runtime::PrepareForExecution() {
  if (shared_readonly_buffer_ && !shared_readonly_buffer_->empty()) {
    GlBuffer shared_buffer;
    RETURN_IF_ERROR(
        shared_readonly_buffer_->CreateSharedGlBuffer(&shared_buffer));
    shared_readonly_buffer_.reset(nullptr);
    RETURN_IF_ERROR(const_objects_.RegisterBuffer(next_const_id_++,
                                                  std::move(shared_buffer)));
  }

  if (options_.reuse_internal_objects) {
    // Analyze internal objects and make a pool of shared objects to be re-used
    // by them. These shared objects need to be allocated upfront.
    std::vector<Object> shared_objects;
    RETURN_IF_ERROR(AssignInternalObjects(&shared_objects));
    for (const Object& object : shared_objects) {
      RETURN_IF_ERROR(AllocateInternalObject(object));
    }
  }

  // Allocate all internal objects and create bindings for them.
  for (auto& program : programs_) {
    for (auto& object : program.refs) {
      // Check whether it is created already.
      BindFunc binding;
      ObjectRef ref = GetRef(object);
      absl::Status status =
          MakeBindingFunc(object, ref, internal_objects_, &binding);
      if (!status.ok()) {
        if (absl::IsNotFound(status)) return status;
        RETURN_IF_ERROR(AllocateInternalObject(object));
        RETURN_IF_ERROR(
            MakeBindingFunc(object, ref, internal_objects_, &binding));
      }
      program.bindings.push_back(std::move(binding));
    }
    program.refs.clear();
  }
  return absl::OkStatus();
}

namespace {

const size_t kNotAssigned = std::numeric_limits<size_t>::max();

struct CombinedUsageRecords {
  std::vector<TensorUsageRecord<size_t>> buffers;
  std::vector<TensorUsageRecord<size_t>> textures_1d;
  std::vector<TensorUsageRecord<uint2>> textures_2d;
  std::vector<TensorUsageRecord<uint3>> textures_3d;
  std::vector<size_t> usage_refs;
};

template <typename TensorSizeT>
void UpdateUsageRecord(TensorUsageRecord<TensorSizeT>* usage_rec,
                       size_t task_id) {
  usage_rec->first_task = std::min(usage_rec->first_task, task_id);
  usage_rec->last_task = std::max(usage_rec->last_task, task_id);
}

struct AddUsageRecordForTextureFunc {
  void operator()(const uint3& size) const {
    auto& usage_ref = usage_records->usage_refs[object_ref];
    if (usage_ref == kNotAssigned) {
      usage_ref = usage_records->textures_3d.size();
      usage_records->textures_3d.emplace_back(/*tensor_size=*/size,
                                              /*first_task=*/program_id,
                                              /*last_task=*/program_id);
    } else {
      UpdateUsageRecord(&usage_records->textures_3d[usage_ref], program_id);
    }
  }

  void operator()(const uint2& size) const {
    auto& usage_ref = usage_records->usage_refs[object_ref];
    if (usage_ref == kNotAssigned) {
      usage_ref = usage_records->textures_2d.size();
      usage_records->textures_2d.emplace_back(/*tensor_size=*/size,
                                              /*first_task=*/program_id,
                                              /*last_task=*/program_id);
    } else {
      UpdateUsageRecord(&usage_records->textures_2d[usage_ref], program_id);
    }
  }

  void operator()(size_t size) const {
    auto& usage_ref = usage_records->usage_refs[object_ref];
    if (usage_ref == kNotAssigned) {
      usage_ref = usage_records->textures_1d.size();
      usage_records->textures_1d.emplace_back(/*tensor_size=*/size,
                                              /*first_task=*/program_id,
                                              /*last_task=*/program_id);
    } else {
      UpdateUsageRecord(&usage_records->textures_1d[usage_ref], program_id);
    }
  }

  CombinedUsageRecords* usage_records;
  const ObjectRef& object_ref;
  const size_t program_id;
};

// We assume that AddUsageRecord for different objects is called in order of
// program_id.
absl::Status AddUsageRecord(CombinedUsageRecords* usage_records,
                            const Object& object, const size_t program_id) {
  auto ref = GetRef(object);
  if (ref >= usage_records->usage_refs.size()) {
    usage_records->usage_refs.resize(ref + 1, kNotAssigned);
  }
  auto& usage_ref = usage_records->usage_refs[ref];
  if (object.object_type == ObjectType::BUFFER) {
    if (usage_ref == kNotAssigned) {
      usage_ref = usage_records->buffers.size();
      usage_records->buffers.emplace_back(
          /*tensor_size=*/NumElements(object.size),
          /*first_task=*/program_id,
          /*last_task=*/program_id);
    } else {
      UpdateUsageRecord(&usage_records->buffers[usage_ref], program_id);
    }
    return absl::OkStatus();
  }
  if (object.object_type == ObjectType::TEXTURE) {
    absl::visit(AddUsageRecordForTextureFunc{usage_records, ref, program_id},
                object.size);
    return absl::OkStatus();
  }
  return absl::InternalError("Unexpected object type");
}

absl::Status ApplyBuffersAssignment(
    const ObjectsAssignment<size_t>& assignment,
    const std::vector<size_t>& global_ref_to_usage_rec,
    const std::vector<Object*>& global_ref_to_object_ptr,
    std::vector<ObjectRef>* global_ref_to_shared_ref,
    std::vector<Object>* shared_objects) {
  std::vector<ObjectRef> assigned_id_to_shared_ref(
      assignment.object_sizes.size(), kInvalidObjectRef);
  for (size_t global_ref = 0; global_ref < global_ref_to_usage_rec.size();
       ++global_ref) {
    const auto& usage_rec_id = global_ref_to_usage_rec[global_ref];
    Object* object = global_ref_to_object_ptr[global_ref];
    if (usage_rec_id == kNotAssigned || object == nullptr ||
        object->object_type != ObjectType::BUFFER) {
      // Skip objects with other data type and non-buffers.
      continue;
    }

    // id of shared object, returned by memory allocation algorithm.
    size_t assigned_id = assignment.object_ids[usage_rec_id];

    // id of corresponding shared object in vector share_objects.
    ObjectRef shared_ref = assigned_id_to_shared_ref[assigned_id];

    if (shared_ref == kInvalidObjectRef) {
      // We need to create new shared object for current buffer.
      shared_ref = shared_objects->size();
      Object shared_object = *object;
      shared_object.access = AccessType::READ_WRITE;
      shared_object.object = shared_ref;
      shared_object.size = assignment.object_sizes[assigned_id];
      shared_objects->push_back(std::move(shared_object));
      assigned_id_to_shared_ref[assigned_id] = shared_ref;
    }
    (*global_ref_to_shared_ref)[global_ref] = shared_ref;
  }
  return absl::OkStatus();
}

template <typename ObjectSizeT>
absl::Status ApplyTexturesAssignment(
    const ObjectsAssignment<ObjectSizeT>& assignment,
    const std::vector<size_t>& global_ref_to_usage_rec,
    const std::vector<Object*>& global_ref_to_object_ptr,
    std::vector<ObjectRef>* global_ref_to_shared_ref,
    std::vector<Object>* shared_objects) {
  std::vector<ObjectRef> assigned_id_to_shared_ref(
      assignment.object_sizes.size(), kInvalidObjectRef);
  for (size_t global_ref = 0; global_ref < global_ref_to_usage_rec.size();
       ++global_ref) {
    const auto& usage_rec_id = global_ref_to_usage_rec[global_ref];
    Object* object = global_ref_to_object_ptr[global_ref];
    if (usage_rec_id == kNotAssigned || object == nullptr ||
        object->object_type != ObjectType::TEXTURE ||
        !absl::holds_alternative<ObjectSizeT>(object->size)) {
      // Skip objects with other data type, non-textures and textures with wrong
      // number of dimensions.
      continue;
    }

    // id of shared object, returned by memory allocation algorithm.
    size_t assigned_id = assignment.object_ids[usage_rec_id];

    // id of corresponding shared object in vector share_objects.
    ObjectRef shared_ref = assigned_id_to_shared_ref[assigned_id];

    if (shared_ref == kInvalidObjectRef) {
      // We need to create new shared object for current texture.
      shared_ref = shared_objects->size();
      Object shared_object = *object;
      shared_object.access = AccessType::READ_WRITE;
      shared_object.object = shared_ref;
      shared_object.size = assignment.object_sizes[assigned_id];
      shared_objects->push_back(std::move(shared_object));
      assigned_id_to_shared_ref[assigned_id] = shared_ref;
    }
    (*global_ref_to_shared_ref)[global_ref] = shared_ref;
  }
  return absl::OkStatus();
}

}  // namespace

// Assign shared objects to internal objects, using memory allocation
// algorithms. Usage records for the algorithms are calculated separately for
// each data type and object type.
absl::Status Runtime::AssignInternalObjects(
    std::vector<Object>* shared_objects) {
  // Build tensor usage records, clusterized by object type and data type.
  std::map<DataType, CombinedUsageRecords> usage_records_by_data_type;
  std::vector<Object*> global_ref_to_object_ptr;
  for (size_t i = 0; i < programs_.size(); ++i) {
    for (auto& object : programs_[i].refs) {
      auto ref = GetRef(object);
      if (ref >= global_ref_to_object_ptr.size()) {
        global_ref_to_object_ptr.resize(ref + 1, nullptr);
      }
      if (global_ref_to_object_ptr[ref] == nullptr) {
        global_ref_to_object_ptr[ref] = &object;
      }
      RETURN_IF_ERROR(AddUsageRecord(
          &usage_records_by_data_type[object.data_type], object, i));
    }
  }

  std::vector<ObjectRef> global_ref_to_shared_ref(
      global_ref_to_object_ptr.size(), kInvalidObjectRef);

  // Calculate and apply shared objects assignment for each data type.
  for (const auto& it : usage_records_by_data_type) {
    const CombinedUsageRecords& usage_records = it.second;
    if (!usage_records.buffers.empty()) {
      ObjectsAssignment<size_t> buffer_assignment;
      RETURN_IF_ERROR(AssignObjectsToTensors(usage_records.buffers,
                                             MemoryStrategy::GREEDY_BEST,
                                             &buffer_assignment));
      RETURN_IF_ERROR(ApplyBuffersAssignment(
          buffer_assignment, usage_records.usage_refs, global_ref_to_object_ptr,
          &global_ref_to_shared_ref, shared_objects));
    }
    if (!usage_records.textures_1d.empty()) {
      ObjectsAssignment<size_t> texture_1d_assignment;
      RETURN_IF_ERROR(AssignObjectsToTensors(usage_records.textures_1d,
                                             MemoryStrategy::GREEDY_BEST,
                                             &texture_1d_assignment));
      RETURN_IF_ERROR(ApplyTexturesAssignment(
          texture_1d_assignment, usage_records.usage_refs,
          global_ref_to_object_ptr, &global_ref_to_shared_ref, shared_objects));
    }
    if (!usage_records.textures_2d.empty()) {
      ObjectsAssignment<uint2> texture_2d_assignment;
      RETURN_IF_ERROR(AssignObjectsToTensors(usage_records.textures_2d,
                                             MemoryStrategy::GREEDY_IN_ORDER,
                                             &texture_2d_assignment));
      RETURN_IF_ERROR(ApplyTexturesAssignment(
          texture_2d_assignment, usage_records.usage_refs,
          global_ref_to_object_ptr, &global_ref_to_shared_ref, shared_objects));
    }
    if (!usage_records.textures_3d.empty()) {
      ObjectsAssignment<uint3> texture_3d_assignment;
      RETURN_IF_ERROR(AssignObjectsToTensors(usage_records.textures_3d,
                                             MemoryStrategy::GREEDY_IN_ORDER,
                                             &texture_3d_assignment));
      RETURN_IF_ERROR(ApplyTexturesAssignment(
          texture_3d_assignment, usage_records.usage_refs,
          global_ref_to_object_ptr, &global_ref_to_shared_ref, shared_objects));
    }
  }

  for (size_t i = 0; i < programs_.size(); ++i) {
    for (auto& object : programs_[i].refs) {
      object.object = global_ref_to_shared_ref[GetRef(object)];
    }
  }
  return absl::OkStatus();
}

absl::Status Runtime::Execute() {
  for (const auto& descriptor : programs_) {
    for (auto& b : descriptor.bindings) {
      RETURN_IF_ERROR(b());
    }
    RETURN_IF_ERROR(command_queue_->Dispatch(descriptor.program,
                                             descriptor.num_workgroups));
  }
  return absl::OkStatus();
}

}  // namespace gl
}  // namespace gpu
}  // namespace tflite
