//TODO: split this method to multiple methods.
        public IList <IAction> Search(Problem p)
        {
            Debug.Assert(p is IBidirectionalProblem);

            searchOutcome = SearchOutcome.PathNotFound;

            ClearInstrumentation();

            var op = ((IBidirectionalProblem)p).GetOriginalProblem();
            var rp = ((IBidirectionalProblem)p).GetReverseProblem();

            var opFrontier = new CachedStateQueue <Node>();
            var rpFrontier = new CachedStateQueue <Node>();

            var ogs = new GraphSearch();
            var rgs = new GraphSearch();

            // Ensure the instrumentation for these
            // are cleared down as their values
            // are used in calculating the overall
            // bidirectional Metrics.
            ogs.ClearInstrumentation();
            rgs.ClearInstrumentation();

            var opNode = new Node(op.InitialState);
            var rpNode = new Node(rp.InitialState);

            opFrontier.Insert(opNode);
            rpFrontier.Insert(rpNode);

            this.SetQueueSize(opFrontier.Size() + rpFrontier.Size());
            this.SetNodesExpanded(ogs.GetNodesExpanded() + rgs.GetNodesExpanded());

            while (!(opFrontier.IsEmpty() && rpFrontier.IsEmpty()))
            {
                // Determine the nodes to work with and expand their fringes
                // in preparation for testing whether or not the two
                // searches meet or one or other is at the GOAL.
                if (!opFrontier.IsEmpty())
                {
                    opNode = opFrontier.Pop();
                    opFrontier.AddAll(ogs.GetResultingNodesToAddToFrontier(opNode, op));
                }
                else
                {
                    opNode = null;
                }
                if (!rpFrontier.IsEmpty())
                {
                    rpNode = rpFrontier.Pop();
                    rpFrontier.AddAll(rgs.GetResultingNodesToAddToFrontier(rpNode, rp));
                }
                else
                {
                    rpNode = null;
                }

                this.SetQueueSize(opFrontier.Size() + rpFrontier.Size());
                this.SetNodesExpanded(ogs.GetNodesExpanded() + rgs.GetNodesExpanded());

                //
                // First Check if either frontier contains the other's state
                if (null != opNode && null != rpNode)
                {
                    Node popNode = null;
                    Node prpNode = null;
                    if (opFrontier.ContainsNodeBasedOn(rpNode.State))
                    {
                        popNode = opFrontier.GetNodeBasedOn(rpNode.State);
                        prpNode = rpNode;
                    }
                    else if (rpFrontier.ContainsNodeBasedOn(opNode.State))
                    {
                        popNode = opNode;
                        prpNode = rpFrontier.GetNodeBasedOn(opNode.State);
                        // Need to also check whether or not the nodes that
                        // have been taken off the frontier actually represent the
                        // same state, otherwise there are instances whereby
                        // the searches can pass each other by
                    }
                    else if (opNode.State.Equals(rpNode.State))
                    {
                        popNode = opNode;
                        prpNode = rpNode;
                    }
                    if (null != popNode && null != prpNode)
                    {
                        IList <IAction> actions = this.RetrieveActions(op, rp, popNode, prpNode);
                        // It may be the case that it is not in fact possible to
                        // traverse from the original node to the goal node based on
                        // the reverse path (i.e. unidirectional links: e.g.
                        // InitialState(A)<->C<-Goal(B) )
                        if (null != actions)
                        {
                            return(actions);
                        }
                    }
                }

                //
                // Check if the original problem is at the GOAL state
                if (null != opNode && SearchUtils.IsGoalState(op, opNode))
                {
                    // No need to check return value for null here
                    // as an action path discovered from the goal
                    // is guaranteed to exist
                    return(this.RetrieveActions(op, rp, opNode, null));
                }
                //
                // Check if the reverse problem is at the GOAL state
                if (null != rpNode && SearchUtils.IsGoalState(rp, rpNode))
                {
                    IList <IAction> actions = this.RetrieveActions(op, rp, null, rpNode);
                    // It may be the case that it is not in fact possible to
                    // traverse from the original node to the goal node based on
                    // the reverse path (i.e. unidirectional links: e.g.
                    // InitialState(A)<-Goal(B) )
                    if (null != actions)
                    {
                        return(actions);
                    }
                }
            }

            // Empty IList can indicate already at Goal
            // or unable to find valid Set of actions
            return(new List <IAction>());
        }