/**
   * Fundamentally, an alignment is just a list of aligned residues in each protein. This method
   * converts two lists of ResidueNumbers into an AFPChain.
   *
   * <p>Parameters are filled with defaults (often null) or sometimes calculated.
   *
   * <p>For a way to modify the alignment of an existing AFPChain, see {@link
   * AlignmentTools#replaceOptAln(AFPChain, Atom[], Atom[], Map)}
   *
   * @param ca1 CA atoms of the first protein
   * @param ca2 CA atoms of the second protein
   * @param aligned1 A list of aligned residues from the first protein
   * @param aligned2 A list of aligned residues from the second protein. Must be the same length as
   *     aligned1.
   * @return An AFPChain representing the alignment. Many properties may be null or another default.
   * @throws StructureException if an error occured during superposition
   * @throws IllegalArgumentException if aligned1 and aligned2 have different lengths
   * @see AlignmentTools#replaceOptAln(AFPChain, Atom[], Atom[], Map)
   */
  public static AFPChain createAFPChain(
      Atom[] ca1, Atom[] ca2, ResidueNumber[] aligned1, ResidueNumber[] aligned2)
      throws StructureException {
    // input validation
    int alnLen = aligned1.length;
    if (alnLen != aligned2.length) {
      throw new IllegalArgumentException("Alignment lengths are not equal");
    }

    AFPChain a = new AFPChain(AFPChain.UNKNOWN_ALGORITHM);
    try {
      a.setName1(ca1[0].getGroup().getChain().getStructure().getName());
      if (ca2[0].getGroup().getChain().getStructure() != null) {
        // common case for cloned ca2
        a.setName2(ca2[0].getGroup().getChain().getStructure().getName());
      }
    } catch (Exception e) {
      // One of the structures wasn't fully created. Ignore
    }
    a.setBlockNum(1);
    a.setCa1Length(ca1.length);
    a.setCa2Length(ca2.length);

    a.setOptLength(alnLen);
    a.setOptLen(new int[] {alnLen});

    Matrix[] ms = new Matrix[a.getBlockNum()];
    a.setBlockRotationMatrix(ms);
    Atom[] blockShiftVector = new Atom[a.getBlockNum()];
    a.setBlockShiftVector(blockShiftVector);

    String[][][] pdbAln = new String[1][2][alnLen];
    for (int i = 0; i < alnLen; i++) {
      pdbAln[0][0][i] = aligned1[i].getChainId() + ":" + aligned1[i];
      pdbAln[0][1][i] = aligned2[i].getChainId() + ":" + aligned2[i];
    }

    a.setPdbAln(pdbAln);

    // convert pdbAln to optAln, and fill in some other basic parameters
    AFPChainXMLParser.rebuildAFPChain(a, ca1, ca2);

    return a;

    // Currently a single block. Split into several blocks by sequence if needed
    //		return AlignmentTools.splitBlocksByTopology(a,ca1,ca2);
  }
  /**
   * After the alignment changes (optAln, optLen, blockNum, at a minimum), many other properties
   * which depend on the superposition will be invalid.
   *
   * <p>This method re-runs a rigid superposition over the whole alignment and repopulates the
   * required properties, including RMSD (TotalRMSD) and TM-Score.
   *
   * @param afpChain
   * @param ca1
   * @param ca2 Second set of ca atoms. Will be modified based on the superposition
   * @throws StructureException
   * @see {@link CECalculator#calc_rmsd(Atom[], Atom[], int, boolean)} contains much of the same
   *     code, but stores results in a CECalculator instance rather than an AFPChain
   */
  public static void updateSuperposition(AFPChain afpChain, Atom[] ca1, Atom[] ca2)
      throws StructureException {

    // Update ca information, because the atom array might also be changed
    afpChain.setCa1Length(ca1.length);
    afpChain.setCa2Length(ca2.length);

    // We need this to get the correct superposition
    int[] focusRes1 = afpChain.getFocusRes1();
    int[] focusRes2 = afpChain.getFocusRes2();
    if (focusRes1 == null) {
      focusRes1 = new int[afpChain.getCa1Length()];
      afpChain.setFocusRes1(focusRes1);
    }
    if (focusRes2 == null) {
      focusRes2 = new int[afpChain.getCa2Length()];
      afpChain.setFocusRes2(focusRes2);
    }

    if (afpChain.getNrEQR() == 0) return;

    // create new arrays for the subset of atoms in the alignment.
    Atom[] ca1aligned = new Atom[afpChain.getOptLength()];
    Atom[] ca2aligned = new Atom[afpChain.getOptLength()];
    int pos = 0;
    int[] blockLens = afpChain.getOptLen();
    int[][][] optAln = afpChain.getOptAln();
    assert (afpChain.getBlockNum() <= optAln.length);

    for (int block = 0; block < afpChain.getBlockNum(); block++) {
      for (int i = 0; i < blockLens[block]; i++) {
        int pos1 = optAln[block][0][i];
        int pos2 = optAln[block][1][i];
        Atom a1 = ca1[pos1];
        Atom a2 = (Atom) ca2[pos2].clone();
        ca1aligned[pos] = a1;
        ca2aligned[pos] = a2;
        pos++;
      }
    }

    // this can happen when we load an old XML serialization which did not support modern ChemComp
    // representation of modified residues.
    if (pos != afpChain.getOptLength()) {
      logger.warn(
          "AFPChainScorer getTMScore: Problems reconstructing alignment! nr of loaded atoms is "
              + pos
              + " but should be "
              + afpChain.getOptLength());
      // we need to resize the array, because we allocated too many atoms earlier on.
      ca1aligned = (Atom[]) resizeArray(ca1aligned, pos);
      ca2aligned = (Atom[]) resizeArray(ca2aligned, pos);
    }

    // Superimpose the two structures in correspondance to the new alignment
    SVDSuperimposer svd = new SVDSuperimposer(ca1aligned, ca2aligned);
    Matrix matrix = svd.getRotation();
    Atom shift = svd.getTranslation();
    Matrix[] blockMxs = new Matrix[afpChain.getBlockNum()];
    Arrays.fill(blockMxs, matrix);
    afpChain.setBlockRotationMatrix(blockMxs);
    Atom[] blockShifts = new Atom[afpChain.getBlockNum()];
    Arrays.fill(blockShifts, shift);
    afpChain.setBlockShiftVector(blockShifts);

    for (Atom a : ca2aligned) {
      Calc.rotate(a, matrix);
      Calc.shift(a, shift);
    }

    // Calculate the RMSD and TM score for the new alignment
    double rmsd = SVDSuperimposer.getRMS(ca1aligned, ca2aligned);
    double tmScore = SVDSuperimposer.getTMScore(ca1aligned, ca2aligned, ca1.length, ca2.length);
    afpChain.setTotalRmsdOpt(rmsd);
    afpChain.setTMScore(tmScore);

    // Calculate the RMSD and TM score for every block of the new alignment
    double[] blockRMSD = new double[afpChain.getBlockNum()];
    double[] blockScore = new double[afpChain.getBlockNum()];
    for (int k = 0; k < afpChain.getBlockNum(); k++) {
      // Create the atom arrays corresponding to the aligned residues in the block
      Atom[] ca1block = new Atom[afpChain.getOptLen()[k]];
      Atom[] ca2block = new Atom[afpChain.getOptLen()[k]];
      int position = 0;
      for (int i = 0; i < blockLens[k]; i++) {
        int pos1 = optAln[k][0][i];
        int pos2 = optAln[k][1][i];
        Atom a1 = ca1[pos1];
        Atom a2 = (Atom) ca2[pos2].clone();
        ca1block[position] = a1;
        ca2block[position] = a2;
        position++;
      }
      if (position != afpChain.getOptLen()[k]) {
        logger.warn(
            "AFPChainScorer getTMScore: Problems reconstructing block alignment! nr of loaded atoms is "
                + pos
                + " but should be "
                + afpChain.getOptLen()[k]);
        // we need to resize the array, because we allocated too many atoms earlier on.
        ca1block = (Atom[]) resizeArray(ca1block, position);
        ca2block = (Atom[]) resizeArray(ca2block, position);
      }
      // Superimpose the two block structures
      SVDSuperimposer svdb = new SVDSuperimposer(ca1block, ca2block);
      Matrix matrixb = svdb.getRotation();
      Atom shiftb = svdb.getTranslation();
      for (Atom a : ca2block) {
        Calc.rotate(a, matrixb);
        Calc.shift(a, shiftb);
      }
      // Calculate the RMSD and TM score for the block
      double rmsdb = SVDSuperimposer.getRMS(ca1block, ca2block);
      double tmScoreb = SVDSuperimposer.getTMScore(ca1block, ca2block, ca1.length, ca2.length);
      blockRMSD[k] = rmsdb;
      blockScore[k] = tmScoreb;
    }
    afpChain.setOptRmsd(blockRMSD);
    afpChain.setBlockRmsd(blockRMSD);
    afpChain.setBlockScore(blockScore);
  }