/**
   * Merges pairs of rows by making them identical and marking them as equivalent. For each merged
   * pair, also merges each pair of not yet merged images by the same generator, and so on
   * recursively, until no more pairs need to be merged.
   *
   * @param Q a list of row pairs to merge.
   * @param P the row equivalence classes (modified by this method).
   * @return the number of individual merges performed.
   */
  private int performIdentifications(final LinkedList Q, final IntPartition P) {
    int count = 0;
    while (Q.size() > 0) {
      final IntPair pair = (IntPair) Q.removeFirst();
      final int a = P.find(pair.getFirst());
      final int b = P.find(pair.getSecond());
      if (a == b) {
        continue;
      }
      P.unite(a, b);
      ++count;

      final int row_a[] = (int[]) this.table.get(a);
      final int row_b[] = (int[]) this.table.get(b);
      for (int g = 0; g < this.ngens; ++g) {
        final int ag = row_a[g];
        final int bg = row_b[g];
        if (ag == 0) {
          row_a[g] = bg;
        } else if (bg == 0) {
          row_b[g] = ag;
        } else if (!P.areEquivalent(ag, bg)) {
          Q.addLast(new IntPair(ag, bg));
        }
      }
      this.table.set(b, row_a);
    }

    return count;
  }
  /**
   * Compresses the table by collapsing each set of rows tagged as equivalent, i.e., representing
   * the same coset, into a single row. At this stage, equivalent rows are expected to have equal
   * contents, as should have been established by {@link #performIdentifications(LinkedList,
   * IntPartition)}.
   *
   * @param P a partition of the row set into equivalence classes.
   * @return a mapping of old to new row numbers.
   */
  private int[] compressTable(final IntPartition P) {
    // --- initialize the mapping from old to new row numbers
    final int old2new[] = new int[this.table.size()];
    // --- initlalize the new coset table
    final LinkedList newTable = new LinkedList();
    // --- the row with number 0 is not used
    newTable.add(this.table.get(0));

    // --- collect the rows for the new table and establish the mapping
    for (int i = 1; i < this.table.size(); ++i) {
      // --- get the representative for i's equivalence class
      final int ri = P.find(i);
      // --- use only the first row in each equivalence class
      if (old2new[ri] == 0) {
        old2new[ri] = newTable.size();
        newTable.add(this.table.get(ri));
      }
      // --- map i to the same row number as its representative
      old2new[i] = old2new[ri];
    }

    // --- copy the new table into the old one
    this.table.clear();
    while (newTable.size() > 0) {
      this.table.add(newTable.removeFirst());
    }

    // --- finally, translate all entries into the new numbering scheme
    for (int i = 1; i < this.table.size(); ++i) {
      final int row[] = (int[]) this.table.get(i);
      for (int j = 0; j < this.ngens; ++j) {
        row[j] = old2new[row[j]];
      }
    }

    // --- return the mapping from old to new row numbers
    return old2new;
  }
  /**
   * Constructs a CosetTable instance with a limit on the number of table rows that may be present
   * at any stage of the construction process. Note that this number may be much higher than the
   * actual number of cosets, because multiple rows may be found correspond to a single coset long
   * after their construction.
   *
   * @param group the group.
   * @param subgroupGenerators generators of the subgroup.
   * @param sizeLimit the limit on the number of coset rows.
   */
  public CosetAction(final FpGroup group, final List subgroupGenerators, final int sizeLimit) {

    // --- copy parameters to fields
    this.group = group;
    this.subgroupGenerators = Collections.unmodifiableList(subgroupGenerators);
    this.sizeLimit = sizeLimit;

    // --- extract generators and relators for group
    final List groupGenerators = group.getGenerators();
    final List groupRelators = group.getRelators();

    // --- set up translation tables
    this.ngens = 2 * groupGenerators.size();
    this.idx2gen = new FreeWord[this.ngens];
    this.gen2idx = new HashMap();
    this.idx2invidx = new int[this.ngens];

    int nu = 0;
    for (Iterator iter = groupGenerators.iterator(); iter.hasNext(); ) {
      final FreeWord g = (FreeWord) iter.next();
      this.idx2gen[nu] = g;
      this.gen2idx.put(g, new Integer(nu));
      this.idx2gen[nu + 1] = g.inverse();
      this.gen2idx.put(g.inverse(), new Integer(nu + 1));
      this.idx2invidx[nu] = nu + 1;
      this.idx2invidx[nu + 1] = nu;
      nu += 2;
    }

    // --- translate relators
    final List rels = new ArrayList();
    for (Iterator iter = groupRelators.iterator(); iter.hasNext(); ) {
      final FreeWord r = (FreeWord) iter.next();
      final int n = r.length();
      for (int i = 0; i < n; ++i) {
        final FreeWord w = r.subword(i, n).times(r.subword(0, i));
        rels.add(translateWord(w));
        rels.add(translateWord(w.inverse()));
      }
    }

    // --- translate subgroup generators
    final List subgens = new ArrayList();
    for (Iterator iter = subgroupGenerators.iterator(); iter.hasNext(); ) {
      final FreeWord generator = (FreeWord) iter.next();
      subgens.add(translateWord(generator));
      subgens.add(translateWord(generator.inverse()));
    }

    // --- set up a coset table with one dummy row and one row for the trivial coset
    this.table = new ArrayList();
    this.table.add(new int[this.ngens]);
    this.table.add(new int[this.ngens]);

    // --- holds classes of rows known to correspond to the same coset
    IntPartition equivalences = new IntPartition();

    // --- number of rows made invalid but not yet deleted
    int invalidRows = 0;

    // --- scan the table row by row, creating and deleting rows on the fly
    int i = 1;
    while (i < this.table.size()) {
      // --- proceed only if the current row is valid
      if (equivalences.find(i) != i) {
        ++i;
        continue;
      }

      // --- scan the current row
      final int row[] = (int[]) this.table.get(i);
      for (int j = 0; j < this.ngens; ++j) {
        // --- proceed only if the current column is empty
        if (row[j] != 0) {
          continue;
        }

        // --- log the result of this construction step
        if (LOGGING) {
          System.out.println("\ni = " + i + ", j = " + j);
        }
        // --- make a new row for the product with g
        final int newRow[] = new int[this.ngens];
        this.table.add(newRow);

        // --- set the correspondences
        final int n = this.table.size();
        row[j] = n - 1;
        newRow[this.idx2invidx[j]] = i;

        if (LOGGING) {
          System.out.println("after setting item:");
          dumpTable();
        }

        // --- scan relations to identify equivalent rows
        final LinkedList identify = new LinkedList();
        for (Iterator iter = rels.iterator(); iter.hasNext(); ) {
          final int rel[] = (int[]) iter.next();
          scanRelation(rel, n - 1, identify);
        }

        // --- scan subgroup generators
        final int one = equivalences.find(1);
        for (Iterator iter = subgens.iterator(); iter.hasNext(); ) {
          final int gen[] = (int[]) iter.next();
          scanRelation(gen, one, identify);
        }

        if (LOGGING) {
          System.out.println("after scanning:");
          dumpTable();
        }

        // --- perform pending identifications
        invalidRows += performIdentifications(identify, equivalences);

        if (LOGGING) {
          System.out.println("after identifying:");
          dumpTable();
        }

        // --- quit if the limit on the number of rows is reached
        if (this.sizeLimit > 0 && n - invalidRows > this.sizeLimit) {
          throw new RuntimeException("table limit reached");
        }

        // --- compress the table if necessary
        if (invalidRows > n / 2) {
          final int old2new[] = compressTable(equivalences);
          equivalences = new IntPartition();
          i = old2new[i];
          invalidRows = 0;
          if (LOGGING) {
            System.out.println("after compressing:");
            dumpTable();
          }
        }
      }

      // --- look at next row
      ++i;
    }

    // --- make a final cleanup
    compressTable(equivalences);
    if (LOGGING) {
      System.out.println("\nAfter final compression:");
      dumpTable();
    }

    // --- some finishing touches
    this.numberOfCosets = this.table.size() - 1;
    this.cosetRepresentatives = computeCosetRepresentatives();
  }