""" @brief  A module for solving frequency pairs yielded by the quantum part of
            Ekerå–Håstad's algorithm [EH17] for computing short discrete
            logarithms. This by using the post-processing algorithms in [E20],
            [E23p] and [E24t] (see Sect. 5.6).

    [E20] Ekerå, M.: "On post-processing in the quantum algorithm for computing
                      short discrete logarithms".
                     Des. Codes Cryptogr. 88, pp. 2313–2335 (2020).

    [E23p] Ekerå, M.: "On the success probability of the quantum algorithm for
                       the short DLP". ArXiv 2309.01754v2 (2025).

    [E24t] Ekerå, M.: "On factoring integers, and computing discrete
                       logarithms and orders, quantumly".
                      PhD thesis, KTH Royal Institute of Technology (2024). """

from enum import Enum;

from math import ceil;

from gmpy2 import mpz;
from gmpy2 import mpfr;
from gmpy2 import mpq;

from gmpy2 import floor as mpfr_floor;
from gmpy2 import rint as mpfr_round;
from gmpy2 import ceil as mpfr_ceil;

from gmpy2 import sqrt as mpfr_sqrt;

from ....math.lattices.babai import babai;

from ....math.lattices.enumerate import enumerate as enumerate_lattice;

from ....math.lattices.lagrange import lagrange;

from ....math.lattices.cvp import solve_cvp;

from ....math.lattices.lll import lll;
from ....math.lattices.lll import LLL_DEFAULT_DELTA;
from ....math.lattices.lll import LLL_DEFAULT_REDUCED_PRECISION;

from ....math.matrices import solve_left;

from ....utils.timeout import Timeout;

from ....math.groups import CyclicGroupElement;

from ....math.modular import truncmod;


class EnumerationOptions(Enum):

  """ @brief  An enumeration of options for solving a list of n frequency pairs
              [[j_1, k_1], ..., [j_n, k_n]] for d.

      The meaning of the options are explained in the documentation for the
      solve_multiple_j_k_for_d() function. """

  TRUE = True;

  FALSE = False;

  CVP = "CVP";

  BOUNDED_BY_TAU = "BOUNDED_BY_TAU";


def expected_u_for_j_k_d(
  j : int | mpz,
  k : int | mpz,
  m : int,
  l : int,
  d : int | mpz,
  tau : int = 0) -> list[int | mpz]:

  """ @brief  Computes the vector u that is associated with a given frequency
              pair (j, k) and logarithm d, when using the post-processing
              algorithm from [E23p] to post-process the output of the quantum
              part of Ekerå–Håstad's algorithm [EH17].

      [EH17] Ekerå, M. and Håstad, J.: "Quantum Algorithms for Computing Short
                                        Discrete Logarithms and Factoring RSA
                                        Integers.". In: PQCrypto 2017.
                                       Springer LNCS 10346, pp. 347–363 (2017).

      [E23p] Ekerå, M.: "On the success probability of the quantum algorithm for
                         the short DLP". ArXiv 2309.01754v2 (2025).

      Recall that the vector v = (truncmod(-2^m k, 2^(m+l)), 0), and that the
      vector u, which is in the lattice L^tau, is such that the difference
      u - v = (truncmod(dj - 2^m k, 2^(m+l)), 2^tau d).

      Given j, k, m, l, d and tau this function returns the vector u.

      @param j  The frequency j. An integer on [0, 2^(m + l)).

      @param k  The frequency k. An integer on [0, 2^l).

      @param m  A positive integer m such that d < 2^m.

      @param l  A positive integer l. The control registers in the quantum part
                of the algorithm are of lengths m + l and l qubits,
                respectively.

      @param d  The discrete logarithm d.

      @param tau  An integer tau on [0, l]. Used to scale the basis for the
                  lattice L^tau that is used in the post-processing, and that is
                  generated by the vectors (j, 2^tau) and (2^(m + l), 0).

      @return   The vector u. """

  return expected_u_for_multiple_j_k_d([[j, k]], m, l, d, tau)


def expected_u_for_multiple_j_k_d(
  j_k_list : list[list[int | mpz]],
  m : int,
  l : int,
  d : int | mpz,
  tau : int = 0) -> list[int | mpz]:

  """ @brief  Computes the vector u that is associated with a given list of n
              frequency pairs [[j_1, k_1], ..., [j_n, k_n]] and logarithm d,
              when using the post-processing algorithm from [E20] or [E23p]
              to post-process the output of the quantum part of Ekerå–Håstad's
              algorithm [EH17].

      [EH17] Ekerå, M. and Håstad, J.: "Quantum Algorithms for Computing Short
                                        Discrete Logarithms and Factoring RSA
                                        Integers.". In: PQCrypto 2017.
                                       Springer LNCS 10346, pp. 347–363 (2017).

      [E20] Ekerå, M.: "On post-processing in the quantum algorithm for
                        computing short discrete logarithms".
                       Des. Codes Cryptogr. 88, pp. 2313–2335 (2020).

      [E23p] Ekerå, M.: "On the success probability of the quantum algorithm for
                         the short DLP". ArXiv 2309.01754v2 (2025).

      Recall that the vector

        v = (truncmod(-2^m k_1, 2^(m+l)), ..., truncmod(-2^m k_n, 2^(m+l)), 0),

      and that the vector u, which is in the lattice L^tau, is such that the
      difference

        u - v = (truncmod(dj_1 - 2^m k_1, 2^(m+l)),
                 ...,
                 truncmod(dj_n - 2^m k_n, 2^(m+l)), 2^tau d).

      Given [[j_1, k_1], ..., [j_n, k_n]], m, l, d and tau this function returns
      the vector u.

      @param j_k_list  The list of n frequencies [[j_1, k_1], ..., [j_n, k_n]].

      @param m  A positive integer m such that d < 2^m.

      @param l  A positive integer l. The control registers in the quantum part
                of the algorithm are of lengths m + l and l qubits,
                respectively.

      @param d  The discrete logarithm d.

      @param tau  An integer tau on [0, l]. Used to scale the basis for the
                  lattice L^tau that is used in the post-processing, and that is
                  generated by the vector (j_1, ..., j_n, 2^tau), and by the n
                  vectors (2^(m + l), 0, ..., 0) thru (0, ..., 0, 2^(m + l), 0).

      @return   The vector u. """

  # Initial setup.
  n = len(j_k_list);

  # Compute the vector u component by component.
  u = [];

  for i in range(n):
    [j_i, k_i] = j_k_list[i];

    v_i = truncmod(-(mpz(2 ** m) * k_i), mpz(2 ** (m + l)));

    t1 = mpz(d * j_i - v_i);
    t2 = truncmod(t1, mpz(2 ** (m + l)));
    mp = mpz(round(mpq(t1 - t2, mpz(2 ** (m + l)))));

    u.append(d * j_i - mpz(2 ** (m + l)) * mp);

  u.append(mpz(2 ** tau) * mpz(d));

  # Return u.
  return u;


def solve_j_k_for_d(
  j : int | mpz,
  k : int | mpz,
  m : int,
  l : int,
  g : CyclicGroupElement,
  x : CyclicGroupElement,
  tau : int,
  t : int = None,
  c : int = 1,
  timeout : int | None | Timeout = None,
  verbose : bool = False) -> int | None:

  """ @brief  Attempts to compute the short discrete logarithm d = log_g x given
              a frequency pair (j, k) yielded by the quantum part of
              Ekerå–Håstad's algorithm [EH17] by using the post-processing
              algorithms in [E23p].

      [EH17] Ekerå, M. and Håstad, J.: "Quantum Algorithms for Computing Short
                                        Discrete Logarithms and Factoring RSA
                                        Integers.". In: PQCrypto 2017.
                                       Springer LNCS 10346, pp. 347–363 (2017).

      [E23p] Ekerå, M.: "On the success probability of the quantum algorithm for
                         the short DLP". ArXiv 2309.01754v2 (2025).

      This function implements the enumeration procedure in Alg. 1 in [E23p]. It
      is guaranteed to recover d if (j, k) is a tau-good pair, and if j is such
      that the lattice L^tau is t-balanced, see Defs. 1–3 in [E23p].

      As shown in Thm. 1 in [E23p], the probability that (j, k) fulfills these
      conditions for t and tau is at least

          max(0, 1 - f(2^tau)) * max(0, 1 - 2^(Delta - 2(t-1) - tau))

      for f(B) = 1 / (B - 1) - 1 / (2 (B - 1)^2) - 1 / (6 (B - 1)^3), and for
      Delta = m - l on [0, m), for m, l parameters to the quantum algorithm.

      Furthermore, as shown in Thm. 1 in [E23p], at most 2^3 c sqrt(N) group
      operations must be performed during the enumeration provided that a few
      group elements are first pre-computed, and provided that there is space to
      store at most 2^3 sqrt(N) / c integers in a lookup table, for c a positive
      integer constant, and for N = 2^(Delta + tau + 1) + 2^(tau + t + 2) + 2.

      @param j  The frequency j. An integer on [0, 2^(m + l)).

      @param k  The frequency k. An integer on [0, 2^l).

      @param m  A positive integer m such that d < 2^m.

      @param l  An integer l = m - Delta for Delta an integer on [0, m). The two
                control registers in the quantum part of the algorithm are of
                lengths m + l and l qubits, respectively.

      @param g  The group element g.

      @param x  The group element x = g^d.

      @param tau  An integer tau on (0, l]. Used to scale the basis for the
                  lattice L^tau that is used in the post-processing, and that
                  is generated by the vectors (j, 2^tau) and (2^(m + l), 0).

      @param t  The parameter t. An integer on [0, m). May be set to None, in
                which case t will be implicitly selected so that the lattice
                L^tau is t-balanced. If t is not set to None, this function
                will return None if the lattice L^tau is not t-balanced.

      @param c  The constant c. A positive integer.

      @param timeout  A timeout after which a TimeoutError will be raised and
                      the computation aborted.

                      The timeout may be represented as an integer specifying
                      the timeout in seconds, or as an instance of the Timeout
                      class. May be set to None, as is the default, in which
                      case no timeout is enforced.

      @param verbose  A flag that may be set to True to print intermediary
                      results and status updates when executing the
                      post-processing algorithm.

      @return   The logarithm d, or None, if solving for d fails. """

  # Sanity checks.
  if not (0 < l <= m):
    raise Exception("Error: Incorrect parameter:",
                      "l must be an integer on (0, m].");

  if not (0 <= tau <= l):
    raise Exception("Error: Incorrect parameter:",
                      "tau must be an integer on [0, l].");

  if not (0 <= t < m):
    raise Exception("Error: Incorrect parameter:",
                      "t must be an integer on [0, m).");

  if not (c >= 1):
    raise Exception("Error: Incorrect parameter:",
                      "c must be a positive integer.");

  if not (0 <= j < (2 ** (m + l))):
    raise Exception("Error: Incorrect parameter:",
                      "j must be an integer on [0, 2^(m + l)).");

  if not (0 <= k < (2 ** l)):
    raise Exception("Error: Incorrect parameter:",
                      "k must be an integer on [0, 2^l).");

  # Initial setup.
  timeout = Timeout.parse(timeout);
  timeout.check();

  j = mpz(j);
  k = mpz(k);

  # Step 1:
  def meet_in_the_middle(
      g : CyclicGroupElement,
      x : CyclicGroupElement,
      nu1 : int | mpz,
      nu2 : int | mpz,
      B1 : int | mpz,
      B2 : int | mpz,
      s1 : mpz,
      s2 : mpz,
      mu : mpq,
      c : int = 1) -> int | None:

    # Step 1.2:
    g1 = g ** s1;
    g2 = g ** s2;
    w = (g1 ** nu1) * (g2 ** nu2) * (x ** -1);

    # Step 1.2:
    n = c * mpz(mpfr_round(mpfr_sqrt(B1 / (B2 + 1))));

    # Step 1.3:
    if verbose:
      print("The first stage begins:", \
              2 * (int(ceil(B1 / n)) - 1), "operation(s)");

    T = dict();
    T[g ** 0] = 0;

    # Step 1.4:
    s = g1 ** n;
    z_plus = s;
    s_inv = s ** -1;
    z_minus = s_inv;

    i = 1;

    # Step 1.5:
    while True:

      timeout.check();

      # Step 1.5.1:
      T[z_plus] = i;
      T[z_minus] = -i;

      # Step 1.5.2:
      i = i + 1;
      if i > mpfr_ceil(B1 / n):
        # Step 1.5.2.1:
        break;

      # Step 1.5.3: Note: s and s^-1 were pre-computed above.
      z_plus = z_plus * s;
      z_minus = z_minus * s_inv;

    # Step 1.6:
    if verbose:
      print("The second stage begins:", \
              2 * B2 + 2 * (B2 + 1) * (n - 1), "operation(s)");

    z_plus = w;
    z_minus = w;
    j = 0;

    # Pre-compute group elements.
    g1_inv = g1 ** -1;
    g2_inv = g2 ** -1;

    g2_g1 = g2 * g1;
    g2_g1_inv = g2 * g1_inv;
    g2_inv_g1 = g2_inv * g1;
    g2_inv_g1_inv = g2_inv * g1_inv;

    # Step 1.7:
    while True:

      # Step 1.7.1:
      zp_plus = z_plus;
      zp_minus = z_minus;
      i = 0;

      # Step 1.7.2:
      while True:

        timeout.check();

        # Step 1.7.2.1:
        if zp_plus in T.keys():
          k = T[zp_plus];

          # Step 1.7.2.1.1:
          d = (nu1 + i - mpz(round(j * mu)) - k * n) * s1 + (nu2 + j) * s2;
          return int(d);

        # Step 1.7.2.2:
        if (j > 0) and (zp_minus in T.keys()):
          k = T[zp_minus];

          # Step 1.7.2.2.1:
          d = (nu1 + i - mpz(round(-j * mu)) - k * n) * s1 + (nu2 - j) * s2;
          return int(d);

        # Step 1.7.2.3:
        i = i + 1;
        if i >= n:
          # Step 1.7.2.3.1:
          break;

        # Step 1.7.2.4: Note: g1 was pre-computed above.
        zp_plus = zp_plus * g1;
        zp_minus = zp_minus * g1;

      # Step 1.7.3:
      j = j + 1;
      if j > B2:
        # Step 1.7.3.1:
        break;

      # Step 1.7.4:

      # Step 1.7.4.1:
      if round(j * mu) < round((j-1) * mu):

        # Step 1.7.4.1.1: Note: g2 g1 and g2^-1 g1^-1 were pre-computed above.
        z_plus = z_plus * g2_g1;
        z_minus = z_minus * g2_inv_g1_inv;

      # Step 1.7.4.2:
      elif round(j * mu) > round((j-1) * mu):

        # Step 1.7.4.2.1: Note: g2 g1^-1 and g2^-1 g1 were pre-computed above.
        z_plus = z_plus * g2_g1_inv;
        z_minus = z_minus * g2_inv_g1;

      # Step 1.7.4.3:
      else:

        # Step 1.7.4.3.1: Note: g2 and g2^-1 were pre-computed above.
        z_plus = z_plus * g2;
        z_minus = z_minus * g2_inv;

    # Step 1.8:
    return None;


  # Step 2: Setup the basis for the lattice.
  A = [[j, mpz(2 ** tau)], [mpz(2 ** (m + l)), mpz(0)]];


  # Step 3: Compute the reduced basis for the lattice and extract s1 and s2.
  [B, _] = lagrange(A);

  s1 = B[0];
  s2 = B[1];

  # Compute the norm lambda1 of s1.
  lambda1 = mpfr_sqrt((s1[0] ** 2) + (s1[1] ** 2));

  # Extra step: Verify the requirement on the norm.
  if None != t:
    if lambda1 < mpz(2 ** (m - t)):
      return None;

  # Step 4:

  # Compute mu, s2_parallel and then s2_perp given s2_parallel.
  mu = mpq(s1[0] * s2[0] + s1[1] * s2[1], s1[0] ** 2 + s1[1] ** 2);

  s2_parallel = [mu * s1[0], mu * s1[1]];
  s2_perp = [s2[0] - s2_parallel[0], s2[1] - s2_parallel[1]];

  # Compute the norm lambda2_perp of s2_perp.
  lambda2_perp = mpfr_sqrt(s2_perp[0] ** 2 + s2_perp[1] ** 2);


  # Step 5:

  # Define the vector v.
  v = [truncmod(-(2 ** m) * k, 2 ** (m + l)), mpz(0)];

  # Use Babai's nearest plane algorithm to find the vector o.
  o = babai(B, v);

  # Let nu1 and nu2 be integers such that o = nu1 * s1 + nu2 * s2.
  nu = solve_left(B, o);


  # Step 6:
  B1 = mpz(mpfr_floor((2 ** (m + tau)) * mpfr_sqrt(mpfr(2)) /
                        mpfr(lambda1) + 1));
  B2 = mpz(mpfr_floor((2 ** (m + tau)) * mpfr_sqrt(mpfr(2)) /
                        mpfr(lambda2_perp) + 1/2));

  if verbose:
    print("Computed B1 =", B1);
    print("Computed B2 =", B2);
    print("");


  # Step 7:
  return meet_in_the_middle(g,
                            x,
                            mpz(nu[0]),
                            mpz(nu[1]),
                            B1,
                            B2,
                            mpz(s1[1] // (2 ** tau)),
                            mpz(s2[1] // (2 ** tau)),
                            mu,
                            c);


def solve_multiple_j_k_for_d(
  j_k_list : list[list[int | mpz] | None],
  m : int,
  l : int,
  g : CyclicGroupElement,
  x : CyclicGroupElement,
  tau : int = 0,
  delta : float = LLL_DEFAULT_DELTA,
  precision : int | None = None,
  enumerate : bool | EnumerationOptions = False,
  timeout : int | None | Timeout = None,
  verbose : bool = False) -> int | mpz | None:

  """ @brief  Attempts to compute the short discrete logarithm d = log_g x given
              a list of n frequency pairs [[j_1, k_1], ..., [j_n, k_n]] yielded
              by n independent runs of the quantum part of Ekerå–Håstad's
              algorithm [EH17] by using the classical post-processing algorithm
              from [E20] and [E24t] (see Sect. 5.6).

      [EH17] Ekerå, M. and Håstad, J.: "Quantum Algorithms for Computing Short
                                        Discrete Logarithms and Factoring RSA
                                        Integers.". In: PQCrypto 2017.
                                       Springer LNCS 10346, pp. 347–363 (2017).

      [E20] Ekerå, M.: "On post-processing in the quantum algorithm for
                        computing short discrete logarithms".
                       Des. Codes Cryptogr. 88, pp. 2313–2335 (2020).

      [E24t] Ekerå, M.: "On factoring integers, and computing discrete
                         logarithms and orders, quantumly".
                        PhD thesis, KTH Royal Institute of Technology (2024).

      @param j_k_list   The n frequency pairs [[j_1, k_1], ..., [j_n, k_n]]
                        where j_1, ..., j_n are integers on [0, 2^(m + l)) and
                        k_1, ..., k_n are integers on [0, 2^l).

      @param m  A positive integer m such that d < 2^m.

      @param l  A positive integer l ≈ m / s for s a tradeoff factor. The two
                control registers in the quantum part of the algorithm are of
                lengths m + l and l qubits, respectively.

      @param g  The group element g.

      @param x  The group element x = g^d.

      @param tau  An integer tau on [0, l]. Used to scale the basis for the
                  lattice L^tau that is used in the post-processing, and that is
                  generated by the vector (j_1, ..., j_n, 2^tau), and by the n
                  vectors (2^(m + l), 0, ..., 0) thru (0, ..., 0, 2^(m + l), 0).

      @param delta  The parameter delta to use when delta-LLL-reducing the basis
                    for the lattice L^tau used in the post-processing. Must be
                    on the interval (1/4, 1]. A polynomial runtime in the
                    dimension of the lattice is only guaranteed for delta < 1.

      @param precision  The precision to use when computing the Gram–Schmidt
                        projection factors as a part of delta-LLL-reducing the
                        basis for the lattice L^tau used in the post-processing.

                        The precision may be set to None, as is the default, in
                        which case the projection factors are represented as
                        exact quotients.

      @param enumerate  A flag that may be set to True to enumerate vectors in
                        the lattice L^tau (until d is found or the specified
                        timeout has elapsed), or to EnumerationOptions.CVP to
                        consider only a closest vector in the lattice as
                        returned by performing a limited enumeration, or to
                        False to consider only the vector returned by Babai's
                        nearest plane algorithm.

                        May also be set to EnumerationOptions.BOUNDED_BY_TAU in
                        which case all vectors within distance R of the origin
                        of the lattice L^tau are enumerated, where R depends on
                        tau as R = sqrt(n + 1) * 2^(m + tau).

      @param timeout  A timeout after which a TimeoutError will be raised and
                      the computation aborted.

                      The timeout may be represented as an integer specifying
                      the timeout in seconds, or as an instance of the Timeout
                      class. May be set to None, as is the default, in which
                      case no timeout is enforced.

      @param verbose  A flag that may be set to True to print intermediary
                      results and status updates when executing the
                      post-processing algorithm.

      @return   The logarithm d, or None, if solving for d fails. """

  # Initial setup.
  timeout = Timeout.parse(timeout);
  timeout.check();

  if None in j_k_list:
    return None;

  n = len(j_k_list);
  if n < 1:
    return None;

  js, ks = zip(*j_k_list)

  # Setup a basis for the lattice.
  if verbose:
    print("Setting up the basis...");

  A = [[mpz(j) for j in js] + [mpz(2 ** tau)]];
  for i in range(0, n):
    row = (n + 1) * [mpz(0)];
    row[i] = mpz(2 ** (m + l));
    A.append(row);

  # Reduce the basis for the lattice.
  if verbose:
    print("Computing the LLL-reduced basis with reduced precision...");

  B = lll(A,
          delta = delta,
          timeout = timeout,
          precision = LLL_DEFAULT_REDUCED_PRECISION);
            # Pre-compute with reduced precision.

  # Check the timeout.
  timeout.check();

  if verbose:
    print(" ** Refining the LLL-reduced basis with full precision...");

  [B, [Bs, M]] = lll(B,
                     delta = delta,
                     timeout = timeout,
                     gs = True,
                     precision = precision);

  # Check the timeout.
  timeout.check();

  # Setup v.
  if verbose:
    print("Setting up the target vector v...");

  v = [truncmod(-(2 ** m) * k, 2 ** (m + l)) for k in ks] + [mpz(0)];

  if enumerate in [EnumerationOptions.TRUE, True,
                   EnumerationOptions.FALSE, False,
                   EnumerationOptions.CVP]:

    # Find a vector u in the lattice that is close to v.
    if enumerate == EnumerationOptions.CVP:
      if verbose:
        print("Executing an enumeration to solve CVP...");

      # Let u be a closest vector to v in the lattice.
      u = solve_cvp(B, v, timeout = timeout, gs = [Bs, M]);
    else:
      if verbose:
        print("Executing Babai's nearest plane algorithm...");

      # Let u be a close vector to v in the lattice.
      u = babai(B, v, Bs);

    # Check if the correct solution was found.
    if verbose:
      print(" ** Checking:", u);

    candidate_d = u[-1];
    if 0 <= candidate_d < 2 ** (m + tau):
      candidate_d = candidate_d // mpz(2 ** tau);
      if g ** candidate_d == x:
        return candidate_d;

    if enumerate not in [True, EnumerationOptions.TRUE]:
      return None;

    # Check the timeout.
    timeout.check();

    if verbose:
      print("Enumerating vectors around v...");

    # Set the enumeration radius R = |u - v|^2.
    R2 = sum([(u[i] - v[i]) ** 2 for i in range(n + 1)]);

    # Enumerate with gradually increasing radii.
    while True:

      # Check the timeout.
      timeout.check();

      # Enumerate, whilst checking the timeout.
      for u in enumerate_lattice(B,
                                 R2,
                                 v,
                                 timeout = timeout,
                                 gs = [Bs, M]):

        # Check if the correct solution was found.
        if verbose:
          print(" ** Checking:", u);

        candidate_d = u[-1];
        if 0 <= candidate_d < 2 ** (m + tau):
          candidate_d = candidate_d // mpz(2 ** tau);
          if g ** candidate_d == x:
            return candidate_d;

      # Double the enumeration radius.
      if verbose:
          print(" ** Doubling the enumeration radius.");

      R2 = 4 * R2;

  elif enumerate == EnumerationOptions.BOUNDED_BY_TAU:

    # Enumerate, whilst checking the timeout.
    for u in enumerate_lattice(B,
                               mpz(n + 1) * mpz(2 ** (2 * (m + tau))),
                               v,
                               timeout = timeout,
                               gs = [Bs, M]):

      # Check if the correct solution was found.
      if verbose:
        print(" ** Checking:", u);

      candidate_d = u[-1];
      if 0 <= candidate_d < 2 ** (m + tau):
        candidate_d = candidate_d // mpz(2 ** tau);
        if g ** candidate_d == x:
          return candidate_d;

    # No solution was found.
    return None;

  else:
    raise Exception("Error: Incorrect parameters: Unknown enumerate option.");