/// <summary>
        /// Recursively builds the projection of a given automaton onto this transducer.
        /// The projected automaton must be epsilon-free.
        /// </summary>
        /// <param name="destAutomaton">The projection being built.</param>
        /// <param name="mappingState">The currently traversed state of the transducer.</param>
        /// <param name="srcState">The currently traversed state of the automaton being projected.</param>
        /// <param name="destStateCache">The cache of the created projection states.</param>
        /// <returns>The state of the projection corresponding to the given mapping state and the position in the projected sequence.</returns>
        private Automaton <TDestSequence, TDestElement, TDestElementDistribution, TDestSequenceManipulator, TDestAutomaton> .State BuildProjectionOfAutomaton(
            TDestAutomaton destAutomaton,
            PairListAutomaton.State mappingState,
            Automaton <TSrcSequence, TSrcElement, TSrcElementDistribution, TSrcSequenceManipulator, TSrcAutomaton> .State srcState,
            Dictionary <IntPair, Automaton <TDestSequence, TDestElement, TDestElementDistribution, TDestSequenceManipulator, TDestAutomaton> .State> destStateCache)
        {
            Debug.Assert(mappingState != null && srcState != null, "Valid states must be provided.");
            Debug.Assert(!ReferenceEquals(srcState.Owner, destAutomaton), "Cannot build a projection in place.");

            //// The code of this method has a lot in common with the code of Automaton<>.BuildProduct.
            //// Unfortunately, it's not clear how to avoid the duplication in the current design.

            // State already exists, return its index
            var statePair = new IntPair(mappingState.Index, srcState.Index);

            Automaton <TDestSequence, TDestElement, TDestElementDistribution, TDestSequenceManipulator, TDestAutomaton> .State destState;
            if (destStateCache.TryGetValue(statePair, out destState))
            {
                return(destState);
            }

            destState = destAutomaton.AddState();
            destStateCache.Add(statePair, destState);

            // Iterate over transitions from mappingState
            for (int mappingTransitionIndex = 0; mappingTransitionIndex < mappingState.TransitionCount; mappingTransitionIndex++)
            {
                var mappingTransition = mappingState.GetTransition(mappingTransitionIndex);
                var childMappingState = mappingState.Owner.States[mappingTransition.DestinationStateIndex];

                // Epsilon transition case
                if (IsSrcEpsilon(mappingTransition))
                {
                    TDestElementDistribution destElementDistribution = mappingTransition.ElementDistribution == null ? null : mappingTransition.ElementDistribution.Second;
                    var childDestState = this.BuildProjectionOfAutomaton(destAutomaton, childMappingState, srcState, destStateCache);
                    destState.AddTransition(destElementDistribution, mappingTransition.Weight, childDestState, mappingTransition.Group);
                    continue;
                }

                // Iterate over states and transitions in the closure of srcState
                for (int srcTransitionIndex = 0; srcTransitionIndex < srcState.TransitionCount; srcTransitionIndex++)
                {
                    var srcTransition = srcState.GetTransition(srcTransitionIndex);
                    Debug.Assert(!srcTransition.IsEpsilon, "The automaton being projected must be epsilon-free.");

                    var srcChildState = srcState.Owner.States[srcTransition.DestinationStateIndex];

                    TDestElementDistribution destElementDistribution;
                    double projectionLogScale = mappingTransition.ElementDistribution.ProjectFirst(
                        srcTransition.ElementDistribution, out destElementDistribution);
                    if (double.IsNegativeInfinity(projectionLogScale))
                    {
                        continue;
                    }

                    Weight destWeight     = Weight.Product(mappingTransition.Weight, srcTransition.Weight, Weight.FromLogValue(projectionLogScale));
                    var    childDestState = this.BuildProjectionOfAutomaton(destAutomaton, childMappingState, srcChildState, destStateCache);
                    destState.AddTransition(destElementDistribution, destWeight, childDestState, mappingTransition.Group);
                }
            }

            destState.EndWeight = Weight.Product(mappingState.EndWeight, srcState.EndWeight);
            return(destState);
        }