/**
   * Assembly the matched linkages.
   *
   * @param matchedAtomsOfLinkages
   * @param mod
   * @param condition
   * @param ret ModifiedCompound will be stored here.
   */
  private void assembleLinkages(
      List<List<Atom[]>> matchedAtomsOfLinkages,
      ProteinModification mod,
      List<ModifiedCompound> ret) {
    ModificationCondition condition = mod.getCondition();
    List<ModificationLinkage> modLinks = condition.getLinkages();

    int nLink = matchedAtomsOfLinkages.size();
    int[] indices = new int[nLink];
    Set<ModifiedCompound> identifiedCompounds = new HashSet<ModifiedCompound>();
    while (indices[0] < matchedAtomsOfLinkages.get(0).size()) {
      List<Atom[]> atomLinkages = new ArrayList<Atom[]>(nLink);
      for (int iLink = 0; iLink < nLink; iLink++) {
        Atom[] atoms = matchedAtomsOfLinkages.get(iLink).get(indices[iLink]);
        atomLinkages.add(atoms);
      }
      if (matchLinkages(modLinks, atomLinkages)) {
        // matched

        int n = atomLinkages.size();
        List<StructureAtomLinkage> linkages = new ArrayList<StructureAtomLinkage>(n);
        for (int i = 0; i < n; i++) {
          Atom[] linkage = atomLinkages.get(i);
          StructureAtomLinkage link =
              StructureUtil.getStructureAtomLinkage(
                  linkage[0], residues.contains(linkage[0].getGroup()),
                  linkage[1], residues.contains(linkage[1].getGroup()));
          linkages.add(link);
        }

        ModifiedCompound mc = new ModifiedCompoundImpl(mod, linkages);
        if (!identifiedCompounds.contains(mc)) {
          ret.add(mc);
          identifiedCompounds.add(mc);
        }
      }

      // indices++ (e.g. [0,0,1]=>[0,0,2]=>[1,2,0])
      int i = nLink - 1;
      while (i >= 0) {
        if (i == 0 || indices[i] < matchedAtomsOfLinkages.get(i).size() - 1) {
          indices[i]++;
          break;
        } else {
          indices[i] = 0;
          i--;
        }
      }
    }
  }
  /**
   * @param modifications a set of {@link ProteinModification}s.
   * @param residues
   * @param ligands
   * @param saveTo save result to
   * @return map from component to list of corresponding residues in the chain.
   */
  private void addModificationGroups(
      final Set<ProteinModification> modifications,
      final List<Group> residues,
      final List<Group> ligands,
      final Map<Component, Set<Group>> saveTo) {
    if (residues == null || ligands == null || modifications == null) {
      throw new IllegalArgumentException("Null argument(s).");
    }

    Map<Component, Set<Component>> mapSingleMultiComps = new HashMap<Component, Set<Component>>();
    for (ProteinModification mod : modifications) {
      ModificationCondition condition = mod.getCondition();
      for (Component comp : condition.getComponents()) {
        for (String pdbccId : comp.getPdbccIds()) {
          Component single =
              Component.of(Collections.singleton(pdbccId), comp.isNTerminal(), comp.isCTerminal());
          Set<Component> mult = mapSingleMultiComps.get(single);
          if (mult == null) {
            mult = new HashSet<Component>();
            mapSingleMultiComps.put(single, mult);
          }
          mult.add(comp);
        }
      }
    }

    {
      // ligands
      Set<Component> ligandsWildCard = mapSingleMultiComps.get(Component.of("*"));
      for (Group group : ligands) {
        String pdbccId = group.getPDBName().trim();
        Set<Component> comps = mapSingleMultiComps.get(Component.of(pdbccId));

        for (Component comp : unionComponentSet(ligandsWildCard, comps)) {
          Set<Group> gs = saveTo.get(comp);
          if (gs == null) {
            gs = new LinkedHashSet<Group>();
            saveTo.put(comp, gs);
          }
          gs.add(group);
        }
      }
    }

    {
      // residues
      if (residues.isEmpty()) {
        return;
      }

      Set<Component> residuesWildCard = mapSingleMultiComps.get(Component.of("*"));

      // for all residues
      for (Group group : residues) {
        String pdbccId = group.getPDBName().trim();
        Set<Component> comps = mapSingleMultiComps.get(Component.of(pdbccId));

        for (Component comp : unionComponentSet(residuesWildCard, comps)) {
          Set<Group> gs = saveTo.get(comp);
          if (gs == null) {
            gs = new LinkedHashSet<Group>();
            saveTo.put(comp, gs);
          }
          gs.add(group);
        }
      }

      // for N-terminal
      int nRes = residues.size();
      int iRes = 0;
      Group res;
      do {
        // for all ligands on N terminal and the first residue
        res = residues.get(iRes++);

        Set<Component> nTermWildCard = mapSingleMultiComps.get(Component.of("*", true, false));

        Set<Component> comps = mapSingleMultiComps.get(Component.of(res.getPDBName(), true, false));

        for (Component comp : unionComponentSet(nTermWildCard, comps)) {
          Set<Group> gs = saveTo.get(comp);
          if (gs == null) {
            gs = new LinkedHashSet<Group>();
            saveTo.put(comp, gs);
          }
          gs.add(res);
        }
      } while (iRes < nRes && ligands.contains(res));

      // for C-terminal
      iRes = residues.size() - 1;
      do {
        // for all ligands on C terminal and the last residue
        res = residues.get(iRes--);

        Set<Component> cTermWildCard = mapSingleMultiComps.get(Component.of("*", false, true));

        Set<Component> comps = mapSingleMultiComps.get(Component.of(res.getPDBName(), false, true));

        for (Component comp : unionComponentSet(cTermWildCard, comps)) {
          Set<Group> gs = saveTo.get(comp);
          if (gs == null) {
            gs = new LinkedHashSet<Group>();
            saveTo.put(comp, gs);
          }
          gs.add(res);
        }
      } while (iRes >= 0 && ligands.contains(res));
    }
  }
  /**
   * Identify a set of modifications in a a list of chains.
   *
   * @param chains query {@link Chain}s.
   * @param potentialModifications query {@link ProteinModification}s.
   */
  public void identify(
      final List<Chain> chains, final Set<ProteinModification> potentialModifications) {

    if (chains == null) {
      throw new IllegalArgumentException("Null structure.");
    }

    if (potentialModifications == null) {
      throw new IllegalArgumentException("Null potentialModifications.");
    }

    reset();

    if (potentialModifications.isEmpty()) {
      return;
    }

    Map<String, Chain> mapChainIdChain = new HashMap<String, Chain>(chains.size());
    residues = new ArrayList<Group>();
    List<Group> ligands = new ArrayList<Group>();
    Map<Component, Set<Group>> mapCompGroups = new HashMap<Component, Set<Group>>();

    for (Chain chain : chains) {
      mapChainIdChain.put(chain.getChainID(), chain);

      List<Group> ress = StructureUtil.getAminoAcids(chain);

      // List<Group> ligs = chain.getAtomLigands();
      List<Group> ligs = StructureTools.filterLigands(chain.getAtomGroups());
      residues.addAll(ress);
      residues.removeAll(ligs);
      ligands.addAll(ligs);
      addModificationGroups(potentialModifications, ress, ligs, mapCompGroups);
    }

    if (residues.isEmpty()) {
      String pdbId = "?";
      if (chains.size() > 0) {
        Structure struc = chains.get(0).getParent();
        if (struc != null) pdbId = struc.getPDBCode();
      }
      logger.warn(
          "No amino acids found for {}. Either you did not parse the PDB file with alignSEQRES records, or this record does not contain any amino acids.",
          pdbId);
    }
    List<ModifiedCompound> modComps = new ArrayList<ModifiedCompound>();

    for (ProteinModification mod : potentialModifications) {
      ModificationCondition condition = mod.getCondition();
      List<Component> components = condition.getComponents();
      if (!mapCompGroups.keySet().containsAll(components)) {
        // not all components exist for this mod.
        continue;
      }

      int sizeComps = components.size();
      if (sizeComps == 1) {

        processCrosslink1(mapCompGroups, modComps, mod, components);

      } else {

        processMultiCrosslink(mapCompGroups, modComps, mod, condition);
      }
    }

    if (recordAdditionalAttachments) {
      // identify additional groups that are not directly attached to amino acids.
      for (ModifiedCompound mc : modComps) {
        identifyAdditionalAttachments(mc, ligands, mapChainIdChain);
      }
    }

    mergeModComps(modComps);

    identifiedModifiedCompounds.addAll(modComps);

    // record unidentifiable linkage
    if (recordUnidentifiableModifiedCompounds) {
      recordUnidentifiableAtomLinkages(modComps, ligands);
      recordUnidentifiableModifiedResidues(modComps);
    }
  }