// Copyright(C) 1999-2020, 2023, 2025 National Technology & Engineering Solutions
// of Sandia, LLC (NTESS).  Under the terms of Contract DE-NA0003525 with
// NTESS, the U.S. Government retains certain rights in this software.
//
// See packages/seacas/LICENSE for details
#include "EJ_CodeTypes.h"
#include "EJ_SystemInterface.h"
#include "EJ_index_sort.h"
#include "EJ_mapping.h"
#include "EJ_vector3d.h"
#include "Ioss_NodeBlock.h"
#include "Ioss_Property.h"
#include "Ioss_Region.h"
#include "Ioss_SmartAssert.h"
#include <algorithm>
#include <cfloat>
#include <cstddef>
#include <fmt/format.h>

namespace {
  template <typename INT>
  void do_matching(std::vector<INT> &i_inrange, const RealVector &i_coord, size_t i_offset,
                   std::vector<INT> &j_inrange, const RealVector &j_coord, size_t j_offset,
                   double epsilon, int XYZ, std::vector<INT> &local_node_map);

  double max3(double x, double y, double z) { return std::max({x, y, z}); }

  void find_range(const std::vector<double> &coord, vector3d &min, vector3d &max)
  {
    if (!coord.empty()) {
      min.set(coord[0], coord[1], coord[2]);
      max = min;
      for (size_t i = 3; i < coord.size(); i += 3) {
        min.x = std::min(min.x, coord[i + 0]);
        min.y = std::min(min.y, coord[i + 1]);
        min.z = std::min(min.z, coord[i + 2]);

        max.x = std::max(max.x, coord[i + 0]);
        max.y = std::max(max.y, coord[i + 1]);
        max.z = std::max(max.z, coord[i + 2]);
      }
    }
    else {
      min.set(0, 0, 0);
      max = min;
    }
  }

  template <typename INT>
  bool handle_omitted_blocks(const RegionVector &part_mesh, std::vector<INT> &local_node_map)
  {
    bool has_omissions = false;
    for (auto &elem : part_mesh) {
      if (elem->get_property("block_omission_count").get_int() > 0) {
        has_omissions = true;
        break;
      }
    }

    if (!has_omissions) {
      for (size_t i = 0; i < local_node_map.size(); i++) {
        local_node_map[i] = i;
      }
    }
    else {
      std::vector<INT> dummy;
      bool             fill_global = false;
      eliminate_omitted_nodes(part_mesh, dummy, local_node_map, fill_global);
      SMART_ASSERT(dummy.empty());

      // The local_node_map is not quite in the correct format after the
      // call to 'eliminate_omitted_nodes'.  We need all non-omitted
      // nodes to have local_node_map[i] == i.
      for (size_t i = 0; i < local_node_map.size(); i++) {
        if (local_node_map[i] >= 0) {
          local_node_map[i] = i;
        }
        else {
          local_node_map[i] = -2; // Need to keep omitted nodes separate from non-nodeset nodes.
        }
      }
    }
    return has_omissions;
  }

  template <typename INT>
  void find_in_range(const std::vector<double> &coord, const vector3d &min, const vector3d &max,
                     std::vector<INT> &in_range)
  {
    if (!coord.empty()) {
      for (size_t i = 0; i < coord.size(); i += 3) {
        if (coord[i + 0] > min.x && coord[i + 0] < max.x && coord[i + 1] > min.y &&
            coord[i + 1] < max.y && coord[i + 2] > min.z && coord[i + 2] < max.z) {
          in_range.push_back(i / 3);
        }
      }
    }
  }

  template <typename INT>
  void match_nodes(const RegionVector &part_mesh, double tolerance,
                   std::vector<INT> &global_node_map, std::vector<INT> &local_node_map)
  {
    size_t part_count = part_mesh.size();
    enum { X = 0, Y = 1, Z = 2 };

    for (size_t ip = 0; ip < part_count; ip++) {
      vector3d            i_max;
      vector3d            i_min;
      std::vector<double> i_coord;
      Ioss::NodeBlock    *inb = part_mesh[ip]->get_node_blocks()[0];
      inb->get_field_data("mesh_model_coordinates", i_coord);
      find_range(i_coord, i_min, i_max);

      size_t i_offset = part_mesh[ip]->get_property("node_offset").get_int();

      for (size_t jp = ip + 1; jp < part_count; jp++) {
        vector3d            j_max;
        vector3d            j_min;
        std::vector<double> j_coord;
        Ioss::NodeBlock    *jnb = part_mesh[jp]->get_node_blocks()[0];
        jnb->get_field_data("mesh_model_coordinates", j_coord);
        find_range(j_coord, j_min, j_max);

        size_t j_offset = part_mesh[jp]->get_property("node_offset").get_int();

        // See if the ranges overlap...
        vector3d max;
        vector3d min;
        max.x = std::min(i_max.x, j_max.x);
        max.y = std::min(i_max.y, j_max.y);
        max.z = std::min(i_max.z, j_max.z);

        min.x = std::max(i_min.x, j_min.x);
        min.y = std::max(i_min.y, j_min.y);
        min.z = std::max(i_min.z, j_min.z);

        double delta[3];
        int    XYZ = X;
        delta[XYZ] = max.x - min.x;
        delta[Y]   = max.y - min.y;
        if (delta[Y] > delta[XYZ]) {
          XYZ = Y;
        }
        delta[Z] = max.z - min.z;
        if (delta[Z] > delta[XYZ]) {
          XYZ = Z;
        }

        double epsilon = (delta[X] + delta[Y] + delta[Z]) / 1.0e3;
        if (epsilon < 0.0) {
          fmt::print("Parts {} and {} do not overlap.\n", ip, jp);
          continue;
        }

        min -= epsilon;
        max += epsilon;

        if (tolerance >= 0.0) {
          epsilon = tolerance;
        }

        std::vector<INT> j_inrange;
        std::vector<INT> i_inrange;

        find_in_range(j_coord, min, max, j_inrange);
        find_in_range(i_coord, min, max, i_inrange);

        // Index sort all nodes on the coordinate range with the maximum delta.
        index_coord_sort(i_coord, i_inrange, XYZ);
        index_coord_sort(j_coord, j_inrange, XYZ);

        if (i_inrange.size() < j_inrange.size()) {
          do_matching(i_inrange, i_coord, i_offset, j_inrange, j_coord, j_offset, epsilon, XYZ,
                      local_node_map);
        }
        else {
          do_matching(j_inrange, j_coord, j_offset, i_inrange, i_coord, i_offset, epsilon, XYZ,
                      local_node_map);
        }
      }
    }

    // Build the global and local maps...
    size_t j = 1;
    for (size_t i = 0; i < local_node_map.size(); i++) {
      if (local_node_map[i] == (INT)i || local_node_map[i] == -1) {
        global_node_map.push_back(j);
        local_node_map[i] = j - 1;
        j++;
      }
      else if (local_node_map[i] >= 0) {
        local_node_map[i] = local_node_map[local_node_map[i]];
      }
    }
  }

} // namespace

template <typename INT>
void match_node_xyz(const RegionVector &part_mesh, double tolerance,
                    std::vector<INT> &global_node_map, std::vector<INT> &local_node_map)
{
  // See if any omitted element blocks...
  handle_omitted_blocks(part_mesh, local_node_map);
  match_nodes(part_mesh, tolerance, global_node_map, local_node_map);
}

template void match_node_xyz(const RegionVector &part_mesh, double tolerance,
                             std::vector<int> &global_node_map, std::vector<int> &local_node_map);
template void match_node_xyz(const RegionVector &part_mesh, double tolerance,
                             std::vector<int64_t> &global_node_map,
                             std::vector<int64_t> &local_node_map);

template <typename INT>
void match_nodeset_nodes(const RegionVector &part_mesh, double tolerance,
                         std::vector<INT> &global_node_map, std::vector<INT> &local_node_map,
                         const SystemInterface &interFace)
{
  // See if any omitted element blocks...
  handle_omitted_blocks(part_mesh, local_node_map);

  // Select all nodes that are in the listed nodeset(s)...
  // On return, `local_node_map` will have >=0 entries for all nodeset nodes.
  // entries = -1 for active, but non-nodeset, and -2 for omitted nodes.
  select_nodeset_nodes(part_mesh, local_node_map, interFace.nodeset_match());

  match_nodes(part_mesh, tolerance, global_node_map, local_node_map);
}

template void match_nodeset_nodes(const RegionVector &part_mesh, double tolerance,
                                  std::vector<int>      &global_node_map,
                                  std::vector<int>      &local_node_map,
                                  const SystemInterface &interFace);
template void match_nodeset_nodes(const RegionVector &part_mesh, double tolerance,
                                  std::vector<int64_t>  &global_node_map,
                                  std::vector<int64_t>  &local_node_map,
                                  const SystemInterface &interFace);
namespace {
  template <typename INT>
  void do_matching(std::vector<INT> &i_inrange, const RealVector &i_coord, size_t i_offset,
                   std::vector<INT> &j_inrange, const RealVector &j_coord, size_t j_offset,
                   double epsilon, int XYZ, std::vector<INT> &local_node_map)
  {
    INT j2beg = 0;

    INT match   = 0;
    INT compare = 0;

    double g_dismin = FLT_MAX;
    double dismax   = -FLT_MAX;

    for (auto ii : i_inrange) {

      if (local_node_map[ii + i_offset] < 0) {
        continue;
      }

      double dismin    = FLT_MAX;
      double dmin      = FLT_MAX;
      INT    node_dmin = -1;

      for (size_t j = j2beg; j < j_inrange.size(); j++) {
        INT jj = j_inrange[j];
        if (jj < 0 || local_node_map[jj + j_offset] < 0) {
          continue;
        }

        if (i_coord[3 * ii + XYZ] - epsilon > j_coord[3 * jj + XYZ]) {
          j2beg = j;
          continue;
        }

        compare++;
        //... Since we are sorted on coordinate X|Y|Z,
        //    if set 'j' X|Y|Z greater than set 'i' X|Y|Z+eps, go to next 'i' X1|Y1|Z1 coord.
        if (j_coord[3 * jj + XYZ] - epsilon > i_coord[3 * ii + XYZ]) {
          break;
        }

        double distance = max3(std::fabs(j_coord[3 * jj + 0] - i_coord[3 * ii + 0]),
                               std::fabs(j_coord[3 * jj + 1] - i_coord[3 * ii + 1]),
                               std::fabs(j_coord[3 * jj + 2] - i_coord[3 * ii + 2]));

        if (float(distance) <= float(epsilon)) {
          if (distance < dmin) {
            dmin      = distance;
            node_dmin = j;
          }
        }
        else {
          if (distance < dismin) {
            dismin = distance;
          }
        }
        if (distance == 0.0) {
          break;
        }
      }

      if (dmin <= epsilon && node_dmin >= 0) {
        INT jnod = j_inrange[node_dmin] + j_offset;
        INT inod = ii + i_offset;
        match++;
        if (dmin > dismax) {
          dismax = dmin;
        }
        j_inrange[node_dmin] *= -1;
        SMART_ASSERT(jnod < (INT)local_node_map.size());
        if (inod < jnod) {
          local_node_map[jnod] = inod;
        }
        else {
          local_node_map[inod] = jnod;
        }
      }
      else {
        if (dismin < g_dismin) {
          g_dismin = dismin;
        }
      }
    }
    if (compare > 0) {
      fmt::print("\nNumber of nodes matched                   = {}\n", match);
      fmt::print("Number of comparisons                     = {}\n", compare);
      fmt::print("Tolerance used for matching               = {}\n", epsilon);
      if (dismax > double(-FLT_MAX)) {
        fmt::print("Maximum distance between matched nodes    = {}\n", dismax);
      }
      if (g_dismin < double(FLT_MAX)) {
        fmt::print("Minimum distance between nonmatched nodes = {}\n", g_dismin);
      }
      fmt::print("\n");
    }
  }
} // namespace
