/**
   * Print an alignment map in a concise representation. Edges are given as two numbers separated by
   * '>'. They are chained together where possible, or separated by spaces where disjoint or
   * branched.
   *
   * <p>Note that more concise representations may be possible. Examples:
   * <li>1>2>3>1
   * <li>1>2>3>2 4>3
   *
   * @param alignment The input function, as a map (see {@link
   *     AlignmentTools#alignmentAsMap(AFPChain)})
   * @param identity An identity-like function providing the isomorphism between the codomain of
   *     alignment (of type <T>) and the domain (type <S>).
   * @return
   */
  public static <S, T> String toConciseAlignmentString(Map<S, T> alignment, Map<T, S> identity) {
    // Clone input to prevent changes
    Map<S, T> alig = new HashMap<S, T>(alignment);

    // Generate inverse alignment
    Map<S, List<S>> inverse = new HashMap<S, List<S>>();
    for (Entry<S, T> e : alig.entrySet()) {
      S val = identity.get(e.getValue());
      if (inverse.containsKey(val)) {
        List<S> l = inverse.get(val);
        l.add(e.getKey());
      } else {
        List<S> l = new ArrayList<S>();
        l.add(e.getKey());
        inverse.put(val, l);
      }
    }

    StringBuilder str = new StringBuilder();

    while (!alig.isEmpty()) {
      // Pick an edge and work upstream to a root or cycle
      S seedNode = alig.keySet().iterator().next();
      S node = seedNode;
      if (inverse.containsKey(seedNode)) {
        node = inverse.get(seedNode).iterator().next();
        while (node != seedNode && inverse.containsKey(node)) {
          node = inverse.get(node).iterator().next();
        }
      }

      // Now work downstream, deleting edges as we go
      seedNode = node;
      str.append(node);

      while (alig.containsKey(node)) {
        S lastNode = node;
        node = identity.get(alig.get(lastNode));

        // Output
        str.append('>');
        str.append(node);

        // Remove edge
        alig.remove(lastNode);
        List<S> inv = inverse.get(node);
        if (inv.size() > 1) {
          inv.remove(node);
        } else {
          inverse.remove(node);
        }
      }
      if (!alig.isEmpty()) {
        str.append(' ');
      }
    }

    return str.toString();
  }
  /**
   * Creates a Map specifying the alignment as a mapping between residue indices of protein 1 and
   * residue indices of protein 2.
   *
   * <p>For example,
   *
   * <pre>
   * 1234
   * 5678</pre>
   *
   * becomes
   *
   * <pre>
   * 1->5
   * 2->6
   * 3->7
   * 4->8</pre>
   *
   * @param afpChain An alignment
   * @return A mapping from aligned residues of protein 1 to their partners in protein 2.
   * @throws StructureException If afpChain is not one-to-one
   */
  public static Map<Integer, Integer> alignmentAsMap(AFPChain afpChain) throws StructureException {
    Map<Integer, Integer> map = new HashMap<Integer, Integer>();

    if (afpChain.getAlnLength() < 1) {
      return map;
    }
    int[][][] optAln = afpChain.getOptAln();
    int[] optLen = afpChain.getOptLen();
    for (int block = 0; block < afpChain.getBlockNum(); block++) {
      for (int pos = 0; pos < optLen[block]; pos++) {
        int res1 = optAln[block][0][pos];
        int res2 = optAln[block][1][pos];
        if (map.containsKey(res1)) {
          throw new StructureException(
              String.format("Residue %d aligned to both %d and %d.", res1, map.get(res1), res2));
        }
        map.put(res1, res2);
      }
    }
    return map;
  }