/// <summary>
 /// Returns a string representation the explanation of how a theorem has been proved.
 /// </summary>
 /// <param name="proof">The theorem proof to be examined.</param>
 /// <param name="formatter">The formatter of the configuration where the proved theorem holds.</param>
 /// <returns>The explanation string.</returns>
 private static string GetProofExplanation(TheoremProof proof, OutputFormatter formatter)
 // Switch based on the type of the used inference rule
 => proof.Rule switch
 {
        /// <summary>
        /// Creates a formatted string describing a given theorem proof.
        /// </summary>
        /// <param name="formatter">The output formatter.</param>
        /// <param name="proof">The theorem proof to be formatted.</param>
        /// <param name="tag">The starting tag of the theorem string. It is empty by default.</param>
        /// <returns>The string representing the theorem proof.</returns>
        public static string FormatTheoremProof(this OutputFormatter formatter, TheoremProof proof, string tag = "")
        {
            // Prepare a dictionary of theorem tags that are used to avoid repetition in the proof tree
            var theoremTags = new Dictionary <Theorem, string>();

            // Local function that recursively converts a given proof to a string, starting the
            // explanation with a given tag
            string FormatTheoremProof(TheoremProof proof, string tag)
            {
                // Start composing the result by formatting the theorem and the proof explanation
                var result = $"{formatter.FormatTheorem(proof.Theorem)} - {GetProofExplanation(proof, formatter)}";

                // If there is nothing left to write, we're done
                if (proof.ProvedAssumptions.Count == 0)
                {
                    return(result);
                }

                // Otherwise we want an empty line
                result += "\n\n";

                // Create an enumerable of reports of the proven assumptions
                var assumptionsString = proof.ProvedAssumptions
                                        // Ordered by theorem
                                        .OrderBy(assumptionProof => formatter.FormatTheorem(assumptionProof.Theorem))
                                        // Process a given one
                                        .Select((assumptionProof, index) =>
                {
                    // Get the theorem for comfort
                    var theorem = assumptionProof.Theorem;

                    // Construct the new tag from the previous one and the ordinal number of the assumption
                    var newTag = $"  {tag}{index + 1}.";

                    // Find out if the theorem already has a tag
                    var theoremIsUntagged = !theoremTags.ContainsKey(theorem);

                    // If the theorem is not tagged yet, tag it
                    if (theoremIsUntagged)
                    {
                        theoremTags.Add(theorem, newTag.Trim());
                    }

                    // Find out the reasoning for the theorem
                    var reasoning = theoremIsUntagged ?
                                    // If it's untagged, recursively find the proof string for it
                                    FormatTheoremProof(assumptionProof, newTag) :
                                    // Otherwise just state it again and add the explanation and the reference to it
                                    $"{formatter.FormatTheorem(theorem)} - {GetProofExplanation(assumptionProof, formatter)} - theorem {theoremTags[theorem]}";

                    // The final result is the new tag (even if it's tagged already) and the reasoning
                    return($"{newTag} {reasoning}");
                });

                // Append the particular assumptions, each on a separate line, while making
                // sure there is exactly one line break at the end
                return($"{result}{assumptionsString.ToJoinedString("\n").TrimEnd()}\n");
            }

            // Call the local recursive function with the passed tag
            return(FormatTheoremProof(proof, tag));
        }