/*
 * Copyright(C) 1999-2020, 2022 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 "defs.h"    // for TRUE, FALSE, max, min
#include "params.h"  // for MAXSETS
#include "structs.h" // for vtx_data

/* Enact a single move from largest (smallest) set in such a way
   that some small (large) set gets bigger. */

int         N_VTX_CHECKS; /* number of considered moves */
int         N_VTX_MOVES;  /* number of actual moves */
static void nextmove(), undo_coupling(), couple();

void movevtxs(struct vtx_data **graph,               /* data structure with vertex weights */
              int               nvtxs,               /* number of vertices in graph */
              int               nsets,               /* how many sets am I dividing into? */
              double           *dist,                /* distances defining splitter */
              int              *indices[][MAXSETS],  /* indices that define order in sorted lists */
              double           *vals[][MAXSETS],     /* values in sorted lists */
              int               startvtx[][MAXSETS], /* index values corresponding to splitter */
              int              *sets,                /* set assignment for each vertex */
              double           *size,                /* sizes of the different sets */
              double           *goal,                /* desired set sizes */
              int               vwgt_max             /* largest vertex weight */
)
{
  double largest;         /* largest overshoot from desired size */
  double smallest;        /* largest undershoot from desired size */
  int    active[MAXSETS]; /* flags sets trying to change size */
  double delta;           /* amount distances must change */
  int    vtx;             /* vertex being moved */
  int    to, from;        /* set vertex is being moved to/from */
  int    weight;          /* weight of vertex being moved */
  int    done;            /* have I successfully move a vertex? */

  /* int npass=0; */  /* counts passes through main loop */
  int    badset = -1; /* most unbalanced set */
  int    toobig = 0;  /* badset too large or too small? */
  int    balanced;    /* is balance attained? */
  double imbalance;   /* amount of imbalance in badset */
  int    i;           /* loop counter */

  /* Find most unbalanced set. */

  imbalance = largest = smallest = 0;
  for (i = 0; i < nsets; i++) {
    if (size[i] - goal[i] > largest) {
      largest = size[i] - goal[i];
      if (largest > imbalance) {
        imbalance = largest;
        badset    = i;
        toobig    = 1;
      }
    }
    else if (goal[i] - size[i] > smallest) {
      smallest = goal[i] - size[i];
      if (smallest > imbalance) {
        imbalance = smallest;
        badset    = i;
        toobig    = -1;
      }
    }
  }
  if (largest + smallest <= vwgt_max) {
    balanced = TRUE;
  }
  else {
    balanced = FALSE;
  }

  /* If not balanced, change distances to move vertices between sets. */
  while (!balanced) {
    /* npass++; */
    for (i = 0; i < nsets; i++) {
      active[i] = FALSE;
    }
    active[badset] = TRUE;

    done = FALSE;
    while (!done) {
      nextmove(nvtxs, nsets, vals, indices, startvtx, dist, sets, toobig, active, &vtx, &to,
               &delta);
      from   = sets[vtx];
      weight = graph[vtx]->vwgt;

      /* Now adjust all active dists to reflect this move so far. */
      for (i = 0; i < nsets; i++) {
        if (active[i]) {
          dist[i] -= toobig * delta;
        }
      }
      if (toobig > 0) {
        if (size[to] + weight - goal[to] < largest) {
          done = TRUE;
          size[from] -= graph[vtx]->vwgt;
          size[to] += graph[vtx]->vwgt;
          sets[vtx] = to;
          undo_coupling(graph, sets, nsets, from, to, toobig, badset, size);
        }
        else {
          couple(nsets, from, to, vtx);
          active[to] = TRUE;
        }
      }
      else {
        if (goal[from] - (size[from] - weight) < smallest) {
          done = TRUE;
          size[from] -= graph[vtx]->vwgt;
          size[to] += graph[vtx]->vwgt;
          sets[vtx] = to;
          undo_coupling(graph, sets, nsets, from, to, toobig, badset, size);
        }
        else {
          couple(nsets, from, to, vtx);
          active[from] = TRUE;
        }
      }
    }

    /* Find most unbalanced set. */
    imbalance = largest = smallest = 0;
    for (i = 0; i < nsets; i++) {
      if (size[i] - goal[i] > largest) {
        largest = size[i] - goal[i];
        if (largest > imbalance) {
          imbalance = largest;
          badset    = i;
          toobig    = 1;
        }
      }
      else if (goal[i] - size[i] > smallest) {
        smallest = goal[i] - size[i];
        if (smallest > imbalance) {
          imbalance = smallest;
          badset    = i;
          toobig    = -1;
        }
      }
    }
    if (largest + smallest <= vwgt_max) {
      balanced = TRUE;
    }
    else {
      balanced = FALSE;
    }
  }
}

/* Find the next move from an active to an inactive set, returning */
/* next_vtx, next_to, and distance. */
static void nextmove(int     nvtxs,               /* number of vertices in graph */
                     int     nsets,               /* how many sets am I dividing into? */
                     double *vals[][MAXSETS],     /* values in sorted lists */
                     int    *indices[][MAXSETS],  /* indices that define order in sorted lists */
                     int     startvtx[][MAXSETS], /* index values corresponding to splitter */
                     double *dist,                /* distances defining splitter */
                     int    *sets,                /* set assignment for each vertex */
                     int     toobig,              /* is bad set too big or too small? */
                     int    *active,              /* flags sets trying to change size */
                     int    *next_vtx,            /* vertex selected to move next */
                     int    *next_to,             /* set vertex should be moved to */
                     double *next_delta           /* size of change in distances */
)
{
  double delta;                    /* amount distance must change */
  double bestdelta;                /* best value see so far */
  int    good;                     /* is this the delta OK? */
  int    first;                    /* is this the first OK delta I've seen? */
  int    maxset, minset;           /* larger/smaller of two sets */
  int    bestfrom = 0, bestto = 0; /* sets best move comes from and goes to */
  int    bestdir = 0;              /* direction to step in list for best move */
  int    bestvtx = 0;              /* vertex being moved between sets */
  int    from, to;                 /* sets vertex wants to move from and to */
  int    index;                    /* offset into indices array */
  int    dir = 0;                  /* direction to step through list */
  int    i, j;                     /* loop counter */

  bestdelta = 0;
  first     = TRUE;
  while (first) {
    for (i = 0; i < nsets; i++) {
      if (active[i]) {
        for (j = 0; j < nsets; j++) {
          if (!active[j]) {
            /* Look for next move from active set i to inactive set j. */
            if (toobig > 0) {
              from = i;
              to   = j;
            }
            else {
              from = j;
              to   = i;
            }
            minset = min(to, from);
            maxset = max(to, from);

            index = startvtx[minset][maxset];
            if (index >= nvtxs || index < 0) {
              good = FALSE;
            }
            else {
              if (j > i) {
                dir = -1;
              }
              else {
                dir = 1;
              }
              good  = TRUE;
              delta = -toobig * dir * vals[minset][maxset][indices[minset][maxset][index]] -
                      (dist[to] - dist[from]);
            }

            if (good && (first || delta < bestdelta)) {
              /* Is this the best so far? */
              first     = FALSE;
              bestdelta = delta;
              bestfrom  = from;
              bestto    = to;
              bestdir   = dir;
              bestvtx   = indices[minset][maxset][index] + 1;
            }
          }
        }
      }
    }

    /* Only accept a vertex if it's from the right set. */
    if (sets[bestvtx] != bestfrom || (toobig > 0 && !active[bestfrom]) ||
        (toobig < 0 && !active[bestto])) {
      /* Set rejection flag, and increment startvtx pointer. */
      first  = TRUE;
      minset = min(bestto, bestfrom);
      maxset = max(bestto, bestfrom);
      startvtx[minset][maxset] -= toobig * bestdir;
    }
    ++N_VTX_CHECKS;
  }
  *next_vtx   = bestvtx;
  *next_to    = bestto;
  *next_delta = bestdelta;
  ++N_VTX_MOVES;
}

static int ncoupled = 0;
static int coupled_vtxs[MAXSETS];
static int coupled_sets[MAXSETS][MAXSETS];

static void couple(int nsets,        /* number of sets being divided into */
                   int from, int to, /* sets to be coupled */
                   int vtx           /* vertex that they share */
)
{
  int i; /* loop counter */

  /* Check for degenerate case of vertex shared by more than two sets. */
  for (i = 0; i < ncoupled; i++) {
    if (coupled_vtxs[i] == vtx) {
      coupled_sets[i][from] = -1;
      coupled_sets[i][to]   = 1;
      return;
    }
  }

  coupled_vtxs[ncoupled] = vtx;
  for (i = 0; i < nsets; i++) {
    coupled_sets[ncoupled][i] = 0;
  }
  coupled_sets[ncoupled][from] = -1;
  coupled_sets[ncoupled][to]   = 1;
  ++ncoupled;
}

static void undo_coupling(struct vtx_data **graph, /* data structure with vertex weights */
                          int              *sets,  /* sets each vertex is in */
                          int               nsets, /* number of sets being divided into */
                          int from, int to,        /* set final vertex moved from and to */
                          int     toobig,          /* are we shrinking or enlarging a set? */
                          int     badset,          /* the set number being shrunk or enlarged */
                          double *size             /* sizes of the different sets */
)
{
  int done;  /* have enough vertices been moved? */
  int found; /* have I found the right set? */
  int vtx;   /* vertex being moved between sets */
  int i, j;  /* loop counter */

  if (ncoupled == 0) {
    return;
  }

  if (toobig > 0) {
    done = FALSE;
    if (from == badset) {
      done = TRUE;
    }
    while (!done) {
      found = FALSE;
      to    = from;
      for (i = 0; i < ncoupled && !found; i++) {
        if (coupled_sets[i][from] == 1) { /* Found an edge into set. */
          found = TRUE;
          /* And find other end of edge. */
          for (j = 0; j < nsets; j++) {
            if (coupled_sets[i][j] == -1) {
              from = j;
            }
          }

          /* Switch the set for this vertex. */
          vtx = coupled_vtxs[i];
          size[from] -= graph[vtx]->vwgt;
          size[to] += graph[vtx]->vwgt;
          sets[vtx]             = to;
          coupled_sets[i][from] = 0;

          if (from == badset) {
            done = TRUE;
          }
        }
      }
    }
  }
  else {
    done = FALSE;
    if (to == badset) {
      done = TRUE;
    }
    while (!done) {
      found = FALSE;
      from  = to;
      for (i = 0; i < ncoupled && !found; i++) {
        if (coupled_sets[i][from] == -1) { /* Found an edge from set. */
          found = TRUE;
          /* And find other end of edge. */
          for (j = 0; j < nsets; j++) {
            if (coupled_sets[i][j] == 1) {
              to = j;
            }
          }

          /* Switch the set for this vertex. */
          vtx = coupled_vtxs[i];
          size[from] -= graph[vtx]->vwgt;
          size[to] += graph[vtx]->vwgt;
          sets[vtx]           = to;
          coupled_sets[i][to] = 0;

          if (to == badset) {
            done = TRUE;
          }
        }
      }
    }
  }

  ncoupled = 0;
}
