/**
   * Builds an {@link AFPChain} from already-matched arrays of atoms and residues.
   *
   * @param ca1 An array of atoms in the first structure
   * @param ca2 An array of atoms in the second structure
   * @param residues1 An array of {@link ResidueNumber ResidueNumbers} in the first structure that
   *     are aligned. Only null ResidueNumbers are considered to be unaligned
   * @param residues2 An array of {@link ResidueNumber ResidueNumbers} in the second structure that
   *     are aligned. Only null ResidueNumbers are considered to be unaligned
   * @throws StructureException
   */
  private static AFPChain buildAlignment(
      Atom[] ca1, Atom[] ca2, ResidueNumber[] residues1, ResidueNumber[] residues2)
      throws StructureException {

    // remove any gap
    // this includes the ones introduced by the nullifying above
    List<ResidueNumber> alignedResiduesList1 = new ArrayList<ResidueNumber>();
    List<ResidueNumber> alignedResiduesList2 = new ArrayList<ResidueNumber>();
    for (int i = 0; i < residues1.length; i++) {
      if (residues1[i] != null && residues2[i] != null) {
        alignedResiduesList1.add(residues1[i]);
        alignedResiduesList2.add(residues2[i]);
      }
    }

    ResidueNumber[] alignedResidues1 =
        alignedResiduesList1.toArray(new ResidueNumber[alignedResiduesList1.size()]);
    ResidueNumber[] alignedResidues2 =
        alignedResiduesList2.toArray(new ResidueNumber[alignedResiduesList2.size()]);

    AFPChain afpChain = AlignmentTools.createAFPChain(ca1, ca2, alignedResidues1, alignedResidues2);
    afpChain.setAlgorithmName("unknown");

    AlignmentTools.updateSuperposition(afpChain, ca1, ca2);

    afpChain.setBlockSize(new int[] {afpChain.getNrEQR()});
    afpChain.setBlockRmsd(new double[] {afpChain.getTotalRmsdOpt()});
    afpChain.setBlockGap(new int[] {afpChain.getGapLen()});

    return afpChain;
  }
  /**
   * Guesses the order of symmetry in an alignment
   *
   * <p>Uses {@link #getSymmetryOrder(Map alignment, Map identity, int, float)} to determine the the
   * symmetry order. For the identity alignment, sorts the aligned residues of each protein
   * sequentially, then defines the ith residues of each protein to be equivalent.
   */
  public static int getSymmetryOrder(AFPChain afpChain, int maxSymmetry, float minimumMetricChange)
      throws StructureException {
    // alignment comes from the afpChain alignment
    Map<Integer, Integer> alignment = AlignmentTools.alignmentAsMap(afpChain);

    // Now construct identity to map aligned residues in sequential order
    Map<Integer, Integer> identity = guessSequentialAlignment(alignment, true);

    return AlignmentTools.getSymmetryOrder(alignment, identity, maxSymmetry, minimumMetricChange);
  }
  /**
   * It replaces an optimal alignment of an AFPChain and calculates all the new alignment scores and
   * variables.
   */
  public static AFPChain replaceOptAln(int[][][] newAlgn, AFPChain afpChain, Atom[] ca1, Atom[] ca2)
      throws StructureException {

    // The order is the number of groups in the newAlgn
    int order = newAlgn.length;

    // Calculate the alignment length from all the subunits lengths
    int[] optLens = new int[order];
    for (int s = 0; s < order; s++) {
      optLens[s] = newAlgn[s][0].length;
    }
    int optLength = 0;
    for (int s = 0; s < order; s++) {
      optLength += optLens[s];
    }

    // Create a copy of the original AFPChain and set everything needed for the structure update
    AFPChain copyAFP = (AFPChain) afpChain.clone();

    // Set the new parameters of the optimal alignment
    copyAFP.setOptLength(optLength);
    copyAFP.setOptLen(optLens);
    copyAFP.setOptAln(newAlgn);

    // Set the block information of the new alignment
    copyAFP.setBlockNum(order);
    copyAFP.setBlockSize(optLens);
    copyAFP.setBlockResList(newAlgn);
    copyAFP.setBlockResSize(optLens);
    copyAFP.setBlockGap(calculateBlockGap(newAlgn));

    // Recalculate properties: superposition, tm-score, etc
    Atom[] ca2clone = StructureTools.cloneAtomArray(ca2); // don't modify ca2 positions
    AlignmentTools.updateSuperposition(copyAFP, ca1, ca2clone);

    // It re-does the sequence alignment strings from the OptAlgn information only
    copyAFP.setAlnsymb(null);
    AFPAlignmentDisplay.getAlign(copyAFP, ca1, ca2clone);

    return copyAFP;
  }
  /**
   * @param afpChain Input afpchain. UNMODIFIED
   * @param ca1
   * @param ca2
   * @param optLens
   * @param optAln
   * @return A NEW AfpChain based off the input but with the optAln modified
   * @throws StructureException if an error occured during superposition
   */
  public static AFPChain replaceOptAln(
      AFPChain afpChain, Atom[] ca1, Atom[] ca2, int blockNum, int[] optLens, int[][][] optAln)
      throws StructureException {
    int optLength = 0;
    for (int blk = 0; blk < blockNum; blk++) {
      optLength += optLens[blk];
    }

    // set everything
    AFPChain refinedAFP = (AFPChain) afpChain.clone();
    refinedAFP.setOptLength(optLength);
    refinedAFP.setBlockSize(optLens);
    refinedAFP.setOptLen(optLens);
    refinedAFP.setOptAln(optAln);
    refinedAFP.setBlockNum(blockNum);

    // TODO recalculate properties: superposition, tm-score, etc
    Atom[] ca2clone = StructureTools.cloneAtomArray(ca2); // don't modify ca2 positions
    AlignmentTools.updateSuperposition(refinedAFP, ca1, ca2clone);

    AFPAlignmentDisplay.getAlign(refinedAFP, ca1, ca2clone);
    return refinedAFP;
  }