/* SPDX-FileCopyrightText: 2024 Blender Authors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

#include <memory>
#include <string>

#include "BLI_assert.h"
#include "BLI_color.hh"
#include "BLI_cpp_type.hh"
#include "BLI_generic_span.hh"
#include "BLI_index_mask.hh"
#include "BLI_map.hh"
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"

#include "FN_multi_function.hh"
#include "FN_multi_function_builder.hh"
#include "FN_multi_function_context.hh"
#include "FN_multi_function_data_type.hh"
#include "FN_multi_function_procedure.hh"
#include "FN_multi_function_procedure_builder.hh"
#include "FN_multi_function_procedure_executor.hh"
#include "FN_multi_function_procedure_optimization.hh"

#include "DNA_node_types.h"

#include "BKE_type_conversions.hh"

#include "NOD_derived_node_tree.hh"
#include "NOD_multi_function.hh"

#include "COM_context.hh"
#include "COM_domain.hh"
#include "COM_input_descriptor.hh"
#include "COM_multi_function_procedure_operation.hh"
#include "COM_pixel_operation.hh"
#include "COM_result.hh"
#include "COM_scheduler.hh"
#include "COM_utilities.hh"

namespace blender::compositor {

using namespace nodes::derived_node_tree_types;

MultiFunctionProcedureOperation::MultiFunctionProcedureOperation(Context &context,
                                                                 PixelCompileUnit &compile_unit,
                                                                 const Schedule &schedule)
    : PixelOperation(context, compile_unit, schedule), procedure_builder_(procedure_)
{
  this->build_procedure();
  procedure_executor_ = std::make_unique<mf::ProcedureExecutor>(procedure_);
}

void MultiFunctionProcedureOperation::execute()
{
  const Domain domain = compute_domain();
  const int64_t size = int64_t(domain.size.x) * domain.size.y;
  const IndexMask mask = IndexMask(size);
  mf::ParamsBuilder parameter_builder{*procedure_executor_, &mask};

  const bool is_single_value = this->is_single_value_operation();

  /* For each of the parameters, either add an input or an output depending on its interface type,
   * allocating the outputs when needed. */
  for (int i = 0; i < procedure_.params().size(); i++) {
    if (procedure_.params()[i].type == mf::ParamType::InterfaceType::Input) {
      const Result &input = get_input(parameter_identifiers_[i]);
      if (input.is_single_value()) {
        parameter_builder.add_readonly_single_input(input.single_value());
      }
      else {
        parameter_builder.add_readonly_single_input(input.cpu_data());
      }
    }
    else {
      Result &output = get_result(parameter_identifiers_[i]);
      if (is_single_value) {
        output.allocate_single_value();
        parameter_builder.add_uninitialized_single_output(
            GMutableSpan(output.get_cpp_type(), output.single_value().get(), 1));
      }
      else {
        output.allocate_texture(domain);
        parameter_builder.add_uninitialized_single_output(output.cpu_data());
      }
    }
  }

  mf::ContextBuilder context_builder;
  procedure_executor_->call_auto(mask, parameter_builder, context_builder);

  /* In case of single value execution, update single value data. */
  if (is_single_value) {
    for (int i = 0; i < procedure_.params().size(); i++) {
      if (procedure_.params()[i].type == mf::ParamType::InterfaceType::Output) {
        Result &output = get_result(parameter_identifiers_[i]);
        output.update_single_value_data();
      }
    }
  }
}

void MultiFunctionProcedureOperation::build_procedure()
{
  for (DNode node : compile_unit_) {
    /* Get the multi-function of the node. */
    auto &multi_function_builder = *node_multi_functions_.lookup_or_add_cb(node, [&]() {
      return std::make_unique<nodes::NodeMultiFunctionBuilder>(*node.bnode(),
                                                               node.context()->btree());
    });
    node->typeinfo->build_multi_function(multi_function_builder);
    const mf::MultiFunction &multi_function = multi_function_builder.function();

    /* Get the variables of the inputs of the node, creating inputs to the operation/procedure if
     * needed. */
    Vector<mf::Variable *> input_variables = this->get_input_variables(node, multi_function);

    /* Call the node multi-function, getting the variables for its outputs. */
    Vector<mf::Variable *> output_variables = procedure_builder_.add_call(multi_function,
                                                                          input_variables);

    /* Assign the output variables to the node's respective outputs, creating outputs for the
     * operation/procedure if needed. */
    this->assign_output_variables(node, output_variables);
  }

  /* Add destructor calls for the variables. */
  for (const auto &item : output_to_variable_map_.items()) {
    /* Variables that are used by the outputs should not be destructed. */
    if (!output_variables_.contains(item.value)) {
      procedure_builder_.add_destruct(*item.value);
    }
  }
  for (mf::Variable *variable : implicit_variables_) {
    /* Variables that are used by the outputs should not be destructed. */
    if (!output_variables_.contains(variable)) {
      procedure_builder_.add_destruct(*variable);
    }
  }
  for (mf::Variable *variable : implicit_input_to_variable_map_.values()) {
    procedure_builder_.add_destruct(*variable);
  }

  mf::ReturnInstruction &return_instruction = procedure_builder_.add_return();
  mf::procedure_optimization::move_destructs_up(procedure_, return_instruction);
  BLI_assert(procedure_.validate());
}

Vector<mf::Variable *> MultiFunctionProcedureOperation::get_input_variables(
    DNode node, const mf::MultiFunction &multi_function)
{
  int available_inputs_index = 0;
  Vector<mf::Variable *> input_variables;
  for (int i = 0; i < node->input_sockets().size(); i++) {
    const DInputSocket input{node.context(), node->input_sockets()[i]};

    if (!is_socket_available(input.bsocket())) {
      continue;
    }

    /* The origin socket is an input, that means the input is unlinked. */
    const DSocket origin = get_input_origin_socket(input);
    if (origin->is_input()) {
      const InputDescriptor origin_descriptor = input_descriptor_from_input_socket(
          origin.bsocket());

      if (origin_descriptor.implicit_input == ImplicitInput::None) {
        /* No implicit input, so get a constant variable that holds the socket value. */
        input_variables.append(this->get_constant_input_variable(DInputSocket(origin)));
      }
      else {
        input_variables.append(this->get_implicit_input_variable(input, DInputSocket(origin)));
      }
    }
    else {
      /* Otherwise, the origin socket is an output, which means it is linked. */
      const DOutputSocket output = DOutputSocket(origin);

      /* If the origin node is part of the multi-function procedure operation, then the output has
       * an existing variable for it. */
      if (compile_unit_.contains(output.node())) {
        input_variables.append(output_to_variable_map_.lookup(output));
      }
      else {
        /* Otherwise, the origin node is not part of the multi-function procedure operation, and a
         * variable that represents an input to the multi-function procedure operation is used. */
        input_variables.append(this->get_multi_function_input_variable(input, output));
      }
    }

    /* We allow multi-functions to use float4 as opposed to ColorSceneLinear4f in their signature
     * for easier development. Float4 and ColorSceneLinear4f will be implicitly converted to one
     * another without information loss, so this flexibility is fine. However, float4 conversion to
     * other types is different than ColorSceneLinear4f conversion to other types, and vice versa.
     * So we need to manually convert to ColorSceneLinear4f if either the input or the origin are
     * color sockets for proper implicit conversion later on. */
    if (get_node_socket_result_type(origin.bsocket()) == ResultType::Color ||
        get_node_socket_result_type(input.bsocket()) == ResultType::Color)
    {
      const mf::DataType expected_type = mf::DataType::ForSingle(
          CPPType::get<ColorSceneLinear4f<eAlpha::Premultiplied>>());
      input_variables.last() = this->convert_variable(input_variables.last(), expected_type);
    }

    /* Implicitly convert the variable type to the expected parameter type if needed. */
    const mf::ParamType parameter_type = multi_function.param_type(available_inputs_index);
    input_variables.last() = this->convert_variable(input_variables.last(),
                                                    parameter_type.data_type());

    available_inputs_index++;
  }

  return input_variables;
}

mf::Variable *MultiFunctionProcedureOperation::get_constant_input_variable(DInputSocket input)
{
  const mf::MultiFunction *constant_function = nullptr;
  switch (input->type) {
    case SOCK_FLOAT: {
      const float value = input->default_value_typed<bNodeSocketValueFloat>()->value;
      constant_function = &procedure_.construct_function<mf::CustomMF_Constant<float>>(value);
      break;
    }
    case SOCK_INT: {
      const int value = input->default_value_typed<bNodeSocketValueInt>()->value;
      constant_function = &procedure_.construct_function<mf::CustomMF_Constant<int32_t>>(value);
      break;
    }
    case SOCK_BOOLEAN: {
      const bool value = input->default_value_typed<bNodeSocketValueBoolean>()->value;
      constant_function = &procedure_.construct_function<mf::CustomMF_Constant<bool>>(value);
      break;
    }
    case SOCK_VECTOR: {
      const float3 value = float3(input->default_value_typed<bNodeSocketValueVector>()->value);
      constant_function = &procedure_.construct_function<mf::CustomMF_Constant<float3>>(value);
      break;
    }
    case SOCK_RGBA: {
      const float4 value = float4(input->default_value_typed<bNodeSocketValueRGBA>()->value);
      constant_function = &procedure_.construct_function<mf::CustomMF_Constant<float4>>(value);
      break;
    }
    default:
      BLI_assert_unreachable();
      break;
  }

  mf::Variable *constant_variable = procedure_builder_.add_call<1>(*constant_function)[0];
  implicit_variables_.append(constant_variable);
  return constant_variable;
}

mf::Variable *MultiFunctionProcedureOperation::get_implicit_input_variable(
    const DInputSocket input, const DInputSocket origin)
{
  const InputDescriptor origin_descriptor = input_descriptor_from_input_socket(origin.bsocket());
  const ImplicitInput implicit_input = origin_descriptor.implicit_input;

  /* Inherit the type and implicit input of the origin input since doing implicit conversion inside
   * the multi-function operation is much cheaper. */
  InputDescriptor input_descriptor = input_descriptor_from_input_socket(input.bsocket());
  input_descriptor.type = origin_descriptor.type;
  input_descriptor.implicit_input = implicit_input;

  /* An input was already declared for that implicit input, so no need to declare it again and we
   * just return its variable.  */
  if (implicit_input_to_variable_map_.contains(implicit_input)) {
    /* But first we update the domain priority of the input descriptor to be the higher priority of
     * the existing descriptor and the descriptor of the new input socket. That's because the same
     * implicit input might be used in inputs inside the multi-function procedure operation which
     * have different priorities. */
    InputDescriptor &existing_input_descriptor = this->get_input_descriptor(
        implicit_inputs_to_input_identifiers_map_.lookup(implicit_input));
    existing_input_descriptor.domain_priority = math::min(
        existing_input_descriptor.domain_priority, input_descriptor.domain_priority);

    return implicit_input_to_variable_map_.lookup(implicit_input);
  }

  const int implicit_input_index = implicit_inputs_to_input_identifiers_map_.size();
  const std::string input_identifier = "implicit_input" + std::to_string(implicit_input_index);
  declare_input_descriptor(input_identifier, input_descriptor);

  /* Map the implicit input to the identifier of the operation input that was declared for it. */
  implicit_inputs_to_input_identifiers_map_.add_new(implicit_input, input_identifier);

  mf::Variable &variable = procedure_builder_.add_input_parameter(
      mf::DataType::ForSingle(Result::cpp_type(input_descriptor.type)), input_identifier);
  parameter_identifiers_.append(input_identifier);

  /* Map the implicit input to the variable that was created for it. */
  implicit_input_to_variable_map_.add(implicit_input, &variable);

  return &variable;
}

mf::Variable *MultiFunctionProcedureOperation::get_multi_function_input_variable(
    DInputSocket input_socket, DOutputSocket output_socket)
{
  /* An input was already declared for that same output socket, so no need to declare it again and
   * we just return its variable.  */
  if (output_to_variable_map_.contains(output_socket)) {
    /* But first we update the domain priority of the input descriptor to be the higher priority of
     * the existing descriptor and the descriptor of the new input socket. That's because the same
     * output might be connected to multiple inputs inside the multi-function procedure operation
     * which have different priorities. */
    const std::string input_identifier = outputs_to_declared_inputs_map_.lookup(output_socket);
    InputDescriptor &input_descriptor = this->get_input_descriptor(input_identifier);
    input_descriptor.domain_priority = math::min(
        input_descriptor.domain_priority,
        input_descriptor_from_input_socket(input_socket.bsocket()).domain_priority);

    /* Increment the input's reference count. */
    inputs_to_reference_counts_map_.lookup(input_identifier)++;

    return output_to_variable_map_.lookup(output_socket);
  }

  const int input_index = inputs_to_linked_outputs_map_.size();
  const std::string input_identifier = "input" + std::to_string(input_index);

  /* Declare the input descriptor for this input and prefer to declare its type to be the same as
   * the type of the output socket because doing type conversion in the multi-function procedure is
   * cheaper. */
  InputDescriptor input_descriptor = input_descriptor_from_input_socket(input_socket.bsocket());
  input_descriptor.type = get_node_socket_result_type(output_socket.bsocket());
  declare_input_descriptor(input_identifier, input_descriptor);

  mf::Variable &variable = procedure_builder_.add_input_parameter(
      mf::DataType::ForSingle(Result::cpp_type(input_descriptor.type)), input_identifier);
  parameter_identifiers_.append(input_identifier);

  /* Map the output socket to the variable that was created for it. */
  output_to_variable_map_.add(output_socket, &variable);

  /* Map the identifier of the operation input to the output socket it is linked to. */
  inputs_to_linked_outputs_map_.add_new(input_identifier, output_socket);

  /* Map the output socket to the identifier of the operation input that was declared for it. */
  outputs_to_declared_inputs_map_.add_new(output_socket, input_identifier);

  /* Map the identifier of the operation input to a reference count of 1, this will later be
   * incremented if that same output was referenced again. */
  inputs_to_reference_counts_map_.add_new(input_identifier, 1);

  return &variable;
}

void MultiFunctionProcedureOperation::assign_output_variables(DNode node,
                                                              Vector<mf::Variable *> &variables)
{
  const DOutputSocket preview_output = find_preview_output_socket(node);

  int available_outputs_index = 0;
  for (int i = 0; i < node->output_sockets().size(); i++) {
    const DOutputSocket output{node.context(), node->output_sockets()[i]};

    if (!is_socket_available(output.bsocket())) {
      continue;
    }

    mf::Variable *output_variable = variables[available_outputs_index];
    output_to_variable_map_.add_new(output, output_variable);

    /* If any of the nodes linked to the output are not part of the multi-function procedure
     * operation but are part of the execution schedule, then an output result needs to be
     * populated for it. */
    const bool is_operation_output = is_output_linked_to_node_conditioned(output, [&](DNode node) {
      return schedule_.contains(node) && !compile_unit_.contains(node);
    });

    /* If the output is used as the node preview, then an output result needs to be populated for
     * it, and we additionally keep track of that output to later compute the previews from. */
    const bool is_preview_output = output == preview_output;
    if (is_preview_output) {
      preview_outputs_.add(output);
    }

    if (is_operation_output || is_preview_output) {
      this->populate_operation_result(output, output_variable);
    }

    available_outputs_index++;
  }
}

void MultiFunctionProcedureOperation::populate_operation_result(DOutputSocket output_socket,
                                                                mf::Variable *variable)
{
  const uint output_id = output_sockets_to_output_identifiers_map_.size();
  const std::string output_identifier = "output" + std::to_string(output_id);

  const ResultType result_type = get_node_socket_result_type(output_socket.bsocket());
  const Result result = context().create_result(result_type);
  populate_result(output_identifier, result);

  /* Map the output socket to the identifier of the newly populated result. */
  output_sockets_to_output_identifiers_map_.add_new(output_socket, output_identifier);

  /* Implicitly convert the variable type to the expected result type if needed. */
  const mf::DataType expected_type = mf::DataType::ForSingle(result.get_cpp_type());
  mf::Variable *converted_variable = this->convert_variable(variable, expected_type);

  procedure_builder_.add_output_parameter(*converted_variable);
  output_variables_.add_new(converted_variable);
  parameter_identifiers_.append(output_identifier);
}

mf::Variable *MultiFunctionProcedureOperation::convert_variable(mf::Variable *variable,
                                                                const mf::DataType expected_type)
{
  const mf::DataType variable_type = variable->data_type();
  if (variable_type == expected_type) {
    return variable;
  }

  const bke::DataTypeConversions &conversion_table = bke::get_implicit_type_conversions();
  const mf::MultiFunction *function = conversion_table.get_conversion_multi_function(
      variable_type, expected_type);

  mf::Variable *converted_variable = procedure_builder_.add_call<1>(*function, {variable})[0];
  implicit_variables_.append(converted_variable);
  return converted_variable;
}

bool MultiFunctionProcedureOperation::is_single_value_operation()
{
  /* Return true if all inputs are single values. */
  for (int i = 0; i < procedure_.params().size(); i++) {
    if (procedure_.params()[i].type == mf::ParamType::InterfaceType::Input) {
      Result &input = this->get_input(parameter_identifiers_[i]);
      if (!input.is_single_value()) {
        return false;
      }
    }
  }
  return true;
}

}  // namespace blender::compositor
